Note: This is a preliminary documentation. It is incomplete and may contain errors and wrong assumptions. Please tell me if you find any mistakes.
The saved game files of Dungeon Master and Chaos Strikes Back share the same general file format with some variants depending on the version and platform of the game.
The saved game files of Dungeon Master II use a different format.
In order to illustrate this specification, I wrote the Saved Game Decoder.
Here is a list of the files described by this specification:
A saved game file consists of several sections. Some sections are encrypted to prevent tampering. For each encrypted section, the file contains a decryption key (1 word) and a stored checksum value (1 word). The decryption algorithm requires the use of the key and computes a new checksum value. It is possible to validate the decryption process by ensuring that the computed checksum value and the stored checksum value are equal.
There is an exception for the section called ‘Block2’ in this specification. The ‘stored checksum value’ must in fact be computed based on all the data in Block1, it is not stored directly as a word. The ‘computed checksum value’ must also be computed separately from the decryption process whose result value is meaningless.
Note: For the purpose of decryption, all the data must be read as words (not single bytes) by following the appropriate endianness of the platform the file was created on.
Here is pseudo-code for the decryption algorithm:
Block[]: An array of word values with index starting at 0.
N: Number of words in the Block[] array
Key: The decryption key
TempValue = Key
ComputedChecksum = Key
For i = 0 To N - 1
ComputedChecksum = (ComputedChecksum + Block[i]) MOD 65536
Block[i] = Block[i] XOR TempValue
ComputedChecksum = (ComputedChecksum + Block[i]) MOD 65536
TempValue = (TempValue + N - i) MOD 65536
Next
Here is pseudo-code to compute the ‘stored checksum value’ for Block2 (working on the original Block1 data read from the saved game file):
Block1[]: An array of 128 word values with index starting at 0.
StoredChecksum = 0
i = 0
Do
StoredChecksum = (StoredChecksum + Block1[i]) Mod 65536
StoredChecksum = StoredChecksum XOR Block1[i + 1]
StoredChecksum = (StoredChecksum - Block1[i + 2] + 65536) Mod 65536
StoredChecksum = StoredChecksum XOR Block1[i + 3]
i = i + 4
Until i = 128
Here is pseudo-code to compute the ‘computed checksum value’ for Block2 (working on the decrypted Block2 data):
Block2[]: An array of 128 word values with index starting at 0.
ComputedChecksum = 0
For i = 0 To 127
ComputedChecksum = (ComputedChecksum + Block2[i]) MOD 65536
Next
The following table shows the structure of a saved game file. Sections are listed in the order in which they appear in the file.
Section name | Size in bytes | Encrypted | Description |
---|---|---|---|
Initial word | 2 bytes | No | This Initial Word is only found in: MINI.DAT from Chaos Strikes Back for X68000 (Value = 5223h) MINI.DAT from Chaos Strikes Back for Amiga Utility Disk English Releases 1,2 and 3 (Value = DEADh) MINIF.DAT from Chaos Strikes Back for Amiga Utility Disk French (Value = 5223h) MINIG.DAT from Chaos Strikes Back for Amiga Utility Disk German (Value = 5223h) This Initial Word is never found in saved games made while playing, even in those created with Chaos Strikes Back for Amiga and X68000. This word does not seem to be used and should be ignored if it is present when reading the file. |
Block1 | 256 bytes | No | Block1 contains random data generated when the game is saved. Only two words are not random: one that contains the Key required to decode Block2 (at offset 20 for Dungeon Master and at offset 58 for Chaos Strikes Back) and the last word of Block1 which value is computed when the game is saved so that the stored checksum value of Block 2 can be computed correctly. |
Block2 | 256 bytes | Yes | Contains the keys and checksums of all other encrypted sections. The structure of this block is slightly different in Dungeon Master and in Chaos Strikes Back. For example, the offsets of keys and checksums are not the same. |
Block3 | 128 bytes | Yes | Contains data about the saved game. |
Creatures Extended Data | 16 bytes per creature located on the same level as the party. | Yes | This 16 bytes structure is called ITEM16 in CSBuild source code. The number of records is stored in Block3. |
Champion Data | 3328 bytes (4 x 800 + 128) or 3324 bytes (4 x 799 + 128) or 1404 bytes (4 x 319 + 128) or 1408 bytes (4 x 320 + 128) | Yes | The size of Champion Data is not the same is all game versions. In some versions, this section contains the champion portraits (size is then 3324 or 3328 bytes) while in other versions the portraits are stored in their own section (size is then 1404 or 1408 bytes). Note that the size of this section does not depend on the number of champions in the party. There is always enough space for 4 champions, filled with zeros when not used. |
Timers Data | 10 bytes per timer entry (All platforms except Apple IIGS) or 16 bytes per timer entry (Apple IIGS) | Yes | The number of Timers is specified in Block3. |
Timers Queue | 2 bytes per timer entry | Yes | The number of Timers is specified in Block3. |
Champion Portraits | 4x464 bytes | No | Contains champion portraits when they are not part of the 'Champions Data' section. This block does not exist when champion portraits are part of the 'Champions Data' section. |
Dungeon Data | x bytes | No | Dungeon Data follows exactly the same format as the dungeon.dat files. It is always uncompressed and with a checksum at the end. Note that it can contain entries in the Clouds and Missile sections, which are always empty in standalone dungeon.dat files but not in saved games. |
Game version | Offset of key to decrypt Block2 | Champion portraits included in Character Data | Size of character data | Endian | Format of Block1/Block2 in CSBwin |
---|---|---|---|---|---|
Dungeon Master for Atari ST 1.0, 1.1, 1.2, 1.3 | 20 | Yes | 3328 | Big | PCGAMEBLOCK1 |
Dungeon Master for Amiga 2.0, 2.1, 2.2 | 20 | Yes | 3328 | Big | PCGAMEBLOCK1 |
Dungeon Master for Amiga 3.6 | 20 | No | 1408 | Big | PCGAMEBLOCK1 |
Dungeon Master for X68000 3.0 | 20 | No | 1408 | Big | PCGAMEBLOCK1 |
Dungeon Master for Apple IIGS 2.0, 2.1 | 20 | Yes | 3324 | Little | PCGAMEBLOCK1 |
Dungeon Master for PC 3.4 | 20 | No | 1404 | Little | PCGAMEBLOCK1 |
Dungeon Master for PC-9801 2.0 | 20 | No | 1404 | Little | PCGAMEBLOCK1 |
Dungeon Master for FM-Towns 2.0 | 20 | No | 1404 | Little | PCGAMEBLOCK1 |
Chaos Strikes Back for Atari ST 2.0, 2.1 | 58 | Yes | 3328 | Big | GAMEBLOCK1 |
Chaos Strikes Back for Amiga 3.1, 3.3, 3.5 | 58 | No | 1408 | Big | GAMEBLOCK1 |
Chaos Strikes Back for X68000 3.1 | 58 | No | 1408 | Big | GAMEBLOCK1 |
Chaos Strikes Back for PC-9801 3.1 | 58 | No | 1404 | Little | GAMEBLOCK1 |
Chaos Strikes Back for FM-Towns 3.1 | 58 | No | 1404 | Little | GAMEBLOCK1 |
Note: The checksum of the Timers Data section is incorrect in the original MINIG.DAT from Chaos Strikes Back for Amiga (German dungeon). All other checksums in this file are correct. Consequently the checksum error must be ignored when decoding this file.
Many information in this section comes directly from the CSBuild source code.
If you want to look at this source code, you can refer to the function named LoadDungeon in LoadDungeon.cpp and to stuctures defined in types.h (like GAMEBLOCK1, PCGAMEBLOCK1 and GAMEBLOCK2).
10 Words: Random data generated each time the game is saved.
1 Word: Key to decrypt Block2
116 Words: Random data generated each time the game is saved.
1 Word: The value of this word is computed so that the checksum value computed on Block1 is identical to the checksum value computed on Block2.
29 Words: Random data generated each time the game is saved.
1 Word: Key to decrypt Block2
97 Words: Random data generated each time the game is saved.
1 Word: The value of this word is computed so that the checksum value computed on Block1 is identical to the checksum value computed on Block2.
struct PCGAMEBLOCK1
{
// Data below this line is 512 bytes long. This
// block is the first read from the game save file.
i8 Byte740[300]; //000
i8 Byte22598; //300
ui8 Byte22596; //301
i8 FILL438[4]; //302
i16 SaveOption; //306
i16 RandomGameID; //308 //i32 RandomGameID; //308 reversed
i16 Block2Hash; //310 swapped
i16 ITEM16Hash; //312 swapped
i16 CharacterHash; //314 swapped
i16 TimersHash; //316 swapped
i16 TimerQueHash; //318 swapped
i32 totalMoveCount;//320
i16 Hash326; //324
i16 Hash328; //326
i16 Hash330; //328
i16 Hash332; //330
i16 Hash334; //332
i16 Hash336; //334
i16 Hash338; //335
i16 Hash340; //338
i16 Hash342; //340
i16 Block2Checksum;//342swapped
i16 ITEM16Checksum; //344 swapped
i16 CharacterChecksum;//346 swapped
i16 TimersChecksum; //348 swapped
i16 TimerQueChecksum; //350 swapped
i16 Checksum354; //352
i16 Checksum356; //354
i16 Checksum358; //356
i16 Checksum360; //358
i16 Checksum362; //360
i16 Checksum364; //362
i16 Checksum366; //364
i16 Checksum368; //366
i16 Checksum370; //368
i16 Checksum372; //370
i16 Checksum374; //372
i16 Word22594; //374 swapped
i16 Word22592; //376 swapped
i8 Byte22808[134]; //378 moved as a unit
};
struct GAMEBLOCK1
{
// Data below this line is 512 bytes long. This
// block is the first read from the game save file.
i8 Byte740[300]; //000
i8 Byte22598; //300
ui8 Byte22596; //301
i8 FILL438[4]; //302
i16 SaveOption; //306
i32 RandomGameID; //308 reversed
i16 Block2Hash; //312 swapped
i16 ITEM16Hash; //314 swapped
i16 CharacterHash; //316 swapped
i16 TimersHash; //318 swapped
i16 TimerQueHash; //320 swapped
i32 totalMoveCount;//322
i16 Hash326; //326
i16 Hash328; //328
i16 Hash330; //330
i16 Hash332; //332
i16 Hash334; //334
i16 Hash336; //336
i16 Hash338; //338
i16 Hash340; //340
i16 Hash342; //342
i16 Block2Checksum;//344swapped
i16 ITEM16Checksum; //346 swapped
i16 CharacterChecksum;//348 swapped
i16 TimersChecksum; //350 swapped
i16 TimerQueChecksum; //352 swapped
i16 Checksum354; //354
i16 Checksum356; //356
i16 Checksum358; //358
i16 Checksum360; //360
i16 Checksum362; //362
i16 Checksum364; //364
i16 Checksum366; //366
i16 Checksum368; //368
i16 Checksum370; //370
i16 Checksum372; //372
i16 Checksum374; //374
i16 Word22594; //376 swapped
i16 Word22592; //378 swapped
i8 Byte22808[132]; //380 moved as a unit
};
struct GAMEBLOCK2
{
// The following 128 bytes are the second thing
// read from the game file.
i32 Time; //000; reversed
i32 ranseed; //004; reversed
ui16 ObjectInHand; //008; swapped
i16 numcharacter; //010; swapped
i16 partyx; //012; swapped
i16 partyy; //014; swapped
i16 partyfacing; //016; swapped
i16 partyLevel; //018; swapped
i16 handChar; //020; swapped Character index
i16 MagicCaster; //022; swapped
i16 NumTimer; //024; swapped
i16 NextAvailTimer;//026; swapped
i16 MaxTimers; //028; swapped
i16 ITEM16QueLen; //030; swapped
i32 Long12950; //032; reversed
i32 Long12954; //036; A timestamp. reversed
i16 Word11710; //040; swapped
i16 Word11712; //042; swapped
i16 Word11714; //044; swapped
i16 MaxITEM16; //046; swapped
i8 FILL180[80]; //048;
};
Offset Size in
in block bytes Description
------------------------------
00 4 B402_longint_GameClock
04 4 B367_longint-LastRandomNumber
08 2 B271_int_PartyLeaderHandObjectID
0A 2 B410_int_PartyChampionsCount
0C 2 B409_int_PartyX
0E 2 B408_int_PartyY
10 2 B407_int_PartyFacing
12 2 B406_int_PartyMapIndex
14 2 B274_int_PartyLeaderChampionIndex
16 2 B162_int_MagicCasterChampionIndex
18 2 B342_int_TimersCount
1A 2 B341_int_FirstUnusedTimerDataIndex
1C 2 B345_uint_TimersMaximumCount
1E 2 B337_uint_ExtendedCreatureDataCount
20 4 B353_longint_LastCreatureAttackTime
24 4 B352_longint_LastPartyMovementTime
28 2 B405_int_DisabledPartyMovementClockTicksCount
2A 2 B404_int_DisabledPartyMovementInThrowDirectionClockTicksCount
2C 2 B403_int_DirectionOfLastThrow
2E 2 B338_int_ExtendedCreatureDataMaximumCount
30 80 00h bytes (unused)
struct ITEM16
{
i16 word0; // monster (DB4) index //swapped when read
private:
ui8 facing_2; // Four 2-bit fields.
// Each member of group is in 2 bits starting from bottom.
ui8 pos_3; // Four 2-bit fields
// The position within the room of each
// member of group. If the monster occupies 2
// corners of the room then its position
// is either 0 or 2.
public:
ui8 uByte4; // Least significant byte of d.Time
// set to d.Time-127
ui8 uByte5; // Direction facing??? Giggler got this set to
// Random(64) + 20. Time to pause????
ui8 uByte6; // mapX square being approached
ui8 uByte7; // mapY square being approached
ui8 uByte8; // compared to new mapX
ui8 uByte9; // compared to new mapY
ui8 uByte10; // mapX
ui8 uByte11; // mapY
ui8 uByte12[4];// Treated as array by TAG00ecca(ProcessTimer25)
inline ui8& positions(void) {return pos_3;};
inline ui8& facings(void) {return facing_2;};
};
// ************* NOTE WELL !! *************
// 3328 bytes read into this part of the structure.
// This includes the four characters and 128 additional
// bytes of global data.
CHARDESC m_Characters[4];
i16 brightness;
i8 SeeThruWalls;
i8 Byte13279;
i16 Word13278; //Party shield
i16 Word13276; //Spell effect
i16 Word13274; //Spell effect
i8 NumHistEnt; // #entries in 13268???
ui8 uByte13271; //Life frozen time
i8 Byte13270; //index of entry in 13268
i8 Byte13269; //index of entry in 13268
i16 Word13268[24]; //history of moves???
// bits 0-4 = mapX
// bits 5_9 = mapY
// bits 10_15 = loaded level
i8 Byte13220[24]; //parallels 13268??
// Is this a once-only kind of thing?
i8 Invisible; // 13196;
i8 Byte13195[41];
///////////// End of Character portion of save file /////////////
// ***************************************************
struct CHARDESC // character info???
{ i8 name[8]; // 00
i8 title[16];// 08 // size??
i16 wordx24; // Not swapped because I don't see it used.
ui8 FILL26[28-26];
ui8 facing; //28
ui8 position; //29
ui8 byte30;
ui8 byte31;
i8 attackType; //32Signed so check for -1 works.
i8 byte33;
i8 incantation[4];//34;
// [0] = power 96 through 111 --> 1 through 6
// [1] = 102 through 107
// [2] = 108 through 113
// [3] = 114 through 119
ui8 FILL38[40-38];
ui8 facing3; //40
ui8 uByte41;
ui8 uByte42; //Poison count. A timer for each one??
ui8 uByte43;
i16 busyTimer;// 44 // Timer Index. Signed so -1 works.
i16 timerIndex;//46;
i16 word48; // ORed with 0x280
// 0x8000 while selecting attack option
i16 ouches; //50;// mask of damaged body parts.
i16 HP; //52
i16 maxHP; //54
i16 stamina; //56;
i16 maxStamina; //58;
i16 mana; //60;
i16 maxMana; //62;
i16 word64;
i16 food; //66;
i16 water; //68;
ATTRIBUTE Attributes[7]; // 70 (maximum, current, minimum)
// 70 = [0] Luck??
// 73 = [1] Strength
// 76 = [2] Dexterity
// 79 = [3] Wisdom
// 82 = [4] Vitality
// 85 = [5] AntiMagic
// 88 = [6] AntiFire
i8 FILL91;
SKILL skills92[20]; // 92 //0 and 4-7 =Fighter
//1 and 8-11 =Ninja
//2 and 12-15=Priest
//3 and 16-19=Wizard
RN possessions[30]; //212
ui16 load;//272; In 10ths of KG
ui16 shieldStrength; //274;
ui8 FILL276[336-276];
i8 portrait[1]; //336
ui8 FILL337[800-337];
};
class ATTRIBUTE // 3-byte structure in character
{
private:
// Let me tell you why I hid these variables.....
// From what I can tell, these are treated as unsigned
// bytes. But at least one of them can go negative.
// The Luck-Minimum starts at 10 and is decreased by
// three for each cursed object. Gather four such
// objects and you have set the minimum to a very
// large number. So I want to handle them exactly the
// same except that when you ask for a number and it
// has gone negative, I will return a zero. But its
// value will export to the Atari perfectly.
ui8 ubMaximum;
ui8 ubCurrent;
ui8 ubMinimum;
}
struct SKILL // 6-byte structure in character
{
i16 word0;
i32 Long2; // Experience in this skill
};
class TIMER
{ // We read them from
// DUNGEON.DAT when game reloaded.
public:
TIMER(void) {uByte5 = uByte6 = uByte7 = uByte8 = uByte9 = 0xcd;};
ui32 time; // Top 8 bits=partyLevel, bottom 24 bits = time
ui8 timerFunction;
// 1
// Happened when I pressed open door switch.
// byte 6 = mapX
// byte 7 = mapY
// byte 9 =
// 2 Delayed door bashing.
// byte 5 = 0
// byte 6 = mapX
// byte 7 = mapY
// 5
//
// byte 6 = mapX
// byte 7 = mapY
// byte 9 = function
// for DB2 fiddle bit 0 of word 2
// = 0 means set
// = 1 means clear
// = 2 means toggle bit 0 of word 2
// for DB3 (Actuator)
// 6 Fiddle with DB2 and DB3
// Appears to toggle things.
// byte 6 = mapX
// byte 7 = mapY
// byte 8 = position when sent to object
// of DB type 2 (text??)
// For DB type 3 (Actuators) the
// position of the actuator is
// ignored. All actuators in the
// target cell receive the timer.
// byte 9 = 0 means set bit
// = 1 means clear
// = 2 means toggle
// When sent to actuator type 14 ...(see actuator type 14)
// 7 Change bit 0x04 in CELLFLAG
// byte 6 = mapX
// byte 7 = mapY
// byte 9 = 0 = Set Unconditionally
// = 1 = If party at X,Y requeue
// If material monster at X,Y requeue
// else clear
// = 2 = Toggle (same rules as set/clear)
// 8 Change bit 0x08 in CELLFLAG.
// If the final state of the bit is a one then the
// function 'ProcessNewlyActiveRoom' is called.
// The timer goes away without restarting anything.
// byte 6 = mapX
// byte 7 = mapY
// byte 9 = tmrAct
// = 0 = tmrAct_SET
// = 1 = tmrAct_CLEAR
// = 2 = tmrAct_TOGGLE
// 9
// byte 6 = mapX
// byte 7 = mapY
// byte 9 =
// 10
// byte 6 = mapX
// byte 7 = mapY
// byte 9 =
// 11 Disable action after casting spell
// byte 5 = chIdx
// byte 6 = 0
// 12 When party moves. Byte 5=chIdx. Removes timerIndex from chIdx
// 13 Happened when I put bones in Altar of Vi.
// byte 5 = character index
// byte 6 = mapX
// byte 7 = mapY
// byte 8 = position
// uByte9 = 0, 1, or 2;
// case 2: TAG00dea8(RNffe4) and requeue type 1 in 5 ticks.
// 22
// Processing for function 22 disabled.
// 24
// byte 6 = mapX
// byte 7 = mapY
// obj 8 = object name
//
// 25
// byte 6 = mapX
// byte 7 = mapY
// object name in word 8
// 29 to 41 have everything to do with monster movement
// This is the heart of the monster AI. Each monster
// gets to do its thing when its timer expires.
// 32 Causes monster action
// byte 6 = mapX
// byte 7 = mapY
// byte 8 = additional time until function changed to 37
// 37 When character dies and bones
// put on floor???
// byte5 comes from Item9660.byte2[0]
// byte6 = mapX
// byte7 = mapY
// bz`z yte 8 = 0
// 48 and 49TAG01826c Missile????
// byte5 = 0
// word6 objectID
// word8 bits 10-11 direction
// bits 5- 9 y
// bits 0- 4 x
// 53 = Watchdog Timer
// 60 and 61 = ???
// After processing this is reset to time+5
// This appears to be a pending monster generator.
// A monster was generated where a monster already
// existed. Wait a while and try again.
// byte6 = mapX
// byte7 = mapY
// obj8 = object to move
// 65
// byte 6 = mapX
// byte 7 = mapY
// All actuators at (X,Y) with actuatorType==0
// get set to actuatorType 6. Used to
// reactivate monster generators and such.
// 70 Adjust Brightness (light level)
// Timer is reset to d.Time+4 if index has
// not been reduced to zero.
// word6 = (+/-)index to d.Byte1074
// Add or subtract from d.13282
//
// 71 Set by 3-2-6 spell (OH EW SAR) invisibility
// 72 Character has used shield potion (Ya potion)
// It is time to remove the shield.
// byte 5 = character index
// word 6 = shield increment to be removed
// 73 set by 3-2-5 spell (OH EW RA See thru walls)
// 74 Set by protection spell
// 75 Character hit by missile??
/// Character Poison effect??
// byte 5 = character index
// word 6 = damage
// Byte 42 of the character has the timer index.
// 77 Some sort of spell will wear off
// d.Word13274 was incremented by word6
// byte 6 = value to subtract from d.Word13274
// 78 Some sort of spell will wear off
// d.Word13276 was incremented by word6
// byte 6 = value to subtract from d.Word13276
// 79 Result of 1-1-5-2 spell ya-bro-ros (magic footprints)
ui8 uByte5;
ui8 uByte6;
ui8 uByte7;
ui8 uByte8;
ui8 uByte9;
No information yet.
Champion Portraits are stored in the same format as they are stored in .CMP files. Each portrait consists of 232 words (464 bytes).
Please refer to the Portrait Files for details.
Dungeon Data follows exactly the same format as the dungeon.dat files. It is always uncompressed and with a checksum at the end.
Note that it can contain entries in the Clouds and Missile sections, which are always empty in standalone dungeon.dat files (not in saved games).
There are also more entries in many other object categories (weapons, armor, etc.) than in the original dungeon file. The engine adds free entries to make place for objects created while playing the game, for example when a creature is killed.
Please refer to the Dungeon Files for details.
I wish to thank Paul R. Stevens for the source code of Chaos Strikes Back for Windows and CSBuild that helped me a lot.