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 Technical Documentation - File Formats - Portrait Files (.CMP) 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 Technical Documentation - File Formats - Dungeon File (DUNGEON.DAT) 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.