Saved games in Doom are stored in a binary format with the DSG extension. There is a total of six slots for saved games, each with the corresponding doomsav*.dsg file, numbered from 0 to 5.
Because Doom originally had to work in rather constrained memory conditions, it had to do writing and reading saved games in a simplistic and straightforward way. For this task, it does not make use of its Zone memory allocation, instead employing a static buffer (in fact, it even reuses a portion of the screen buffers memory) and writing/reading into it byte-by-byte. While it works much faster than the direct file input and output on an old system like DOS, it has an inherent problem of possibly overrunning the static buffer. The buffer size is set at 180,244 bytes (0x2c000) and, unlike with the demo recording, cannot be increased by the user. With large and complex levels the amount of data that must be saved can easily exceed that amount, causing a savegame buffer overflow. In this case the game exits with an error message.
The following are the details of the savegame format. Because most of its data is interpreted by the engine in a programmatical way (it loads the level and restores the game state by going through its aspects), this virtually nullifies the feasibility of the manual hacking of saved games, one exception being the player data.
40 bytes in size.
|0x00||24||savename||Savegame description string, padded with zeroes if less than 24 bytes.|
|0x18||16||version||Game version string, in format "version %i". This stores the internal engine version, e.g. 109 for 1.9, or 110 for LinuxDoom. If savegame version differs from that of the engine, the loading is aborted, though no error message is displayed.|
General level parameters
10 bytes in size.
|0x28||1||gameskill||a skill of the game, 1-5|
|0x29||1||gameepisode||an episode of the game, 1-4|
|0x2a||1||gamemap||current level, 1-32|
|0x2b||1x4||playeringame||boolean byte flag for each connected player.|
|0x2f||3||leveltime||Number of tics since the level was started. (least significant byte last, first is unused, so max stored time must be 0xFFFFFF?)|
Each player block is 280 bytes in size, padded to 4 bytes. First block is always present; the rest are saved only if corresponding player is currently in game. When loading player data, Doom does not actually check for connected network nodes, simply placing and setting up map things according to the saved game's contents.
TODO: what actually happens after loading when there are not enough players connected?
The following is the binary contents of the player_t structure for the first player. Following player blocks, if any, are offset by 0x118 each.
|0x32||4||mo||pointer to the player's mobj_t body|
|0x3e||4||cmd||ticcmd_t (contains player input)|
|0x42||4||viewz||absolute height of the player's view point (viewheight + mo->z)|
|0x46||4||viewheight||relative height of the player's view|
|0x4a||4||deltaviewheight||delta to apply to the player's view for temporary changes such as hitting the floor|
|0x4e||4||bob||weapon sprite bobbing amount|
|0x52||4||health||mirror of mo->health, used by status bar and during intermissions|
|0x56||4||armorpoints||amount of armor possessed|
|0x5a||4||armortype||0 for none, 1 for green, 2 for blue|
|0x5e||4||powers||remaining time of the invulnerability powerup (tics)|
|0x62||4||true if the berserk powerup is active (also used for the red haze transitions)|
|0x66||4||remaining time of the partial invisibility powerup (tics)|
|0x6a||4||remaining time of the radiation suit powerup (tics)|
|0x6e||4||true if the player has the computer map powerup|
|0x72||4||remaining time of the light amplification visor powerup (tics)|
|0x8e||4||backpack||true if the player has the backpack|
|0x8e||4x4||frags||frags for each player; actual frag counts are calculated on the fly by adding these numbers together in a loop|
|0xa2||4||readyweapon||weapon currently equipped|
|0xa6||4||pendingweapon||pending weapon (10 if not changing to a new weapon)|
|0xaa||4x9||weaponowned||weapons owned (1-9)|
|0xce||4x4||ammo||ammo counts (1-4)|
|0xde||4x4||maxammo||max ammo capacities|
|0xee||4||attackdown||attack button held down|
|0xf2||4||usedown||use button held down|
|0xf6||4||cheats||bitmask for cheats currently active|
|0xfa||4||refire||1 if the player has been firing for some time (diminished accuracy)|
|0xfe||4||killcount||number of monsters killed|
|0x102||4||itemcount||number of items collected|
|0x106||4||secretcount||number of secrets discovered|
|0x10a||4||message||pointer to the last displayed message|
|0x10e||4||damagecount||number used to calculate index into PLAYPAL for damage|
|0x112||4||bonuscount||number used to calculate PLAYPAL index for powerup pickup flash|
|0x116||4||attacker||pointer to last attacker mobj_t|
|0x11a||4||extralight||Amount to add to sector light levels for gun flashes|
|0x122||4||colormap||used to remap the colors of the player's sprite|
|0x132||16x2||psprites||player weapon sprite and flash sprite data|
|0x146||4||didsecret||true if the player has visited the secret level|
This (and subsequent) blocks may greatly vary in size according to the map data and current playsim state, thus their offsets must be calculated. This also makes manual hacking of these virtually impossible.
Each entry is 14 bytes in size, number of entries is the length of the SECTORS lump divided by the 26 bytes.
|0x04||2||floorpic||internal flat index for floor (not lump name)|
|0x06||2||ceilingpic||internal flat index for ceiling|
An entry's size is 16 bytes for one-sided lines, and 26 bytes for two-sided ones.
The number of entries is the length of the LINEDEFS lump divided by 14 bytes.
|Note: the sidedef order is front, then back. A given side's data is not stored if the line does not have a valid one defined.|
|0x06||2||textureoffset||horizontal offset (in units)|
|0x08||2||rowoffset||vertical offset (in units)|
|0x0a||2||toptexture||internal index of upper texture|
|0x0c||2||bottomtexture||internal index of lower texture|
|0x0e||2||midtexture||internal index of middle texture|
|0x10||2||textureoffset||horizontal offset (in units)|
|0x12||2||rowoffset||vertical offset (in units)|
|0x14||2||toptexture||internal index of upper texture|
|0x16||2||bottomtexture||internal index of lower texture|
|0x18||2||midtexture||internal index of middle texture|
Every mobj_t in the thinker list is saved. Each entry is 161 bytes, padded to 4.
|0x00||1||thinkerclass||1 to continue reading, 0 to terminate the list|
|0x01||4x3||thinker||thinker_t structure (contains linked list pointers and the address of the P_MobjThinker routine)|
|0x19||4||snext||links in sector thinglist|
|0x21||4||angle||orientation as a binary angle measure (1 degree = 0xb60b60)|
|0x29||4||frame||frame index (also stores fullbright as a bitflag)|
|0x2d||4||bnext||links to next object in the same BLOCKMAP cell|
|0x31||4||subsector||pointer to the object's subsector|
|0x35||4||floorz||the closest floor level over all contacted sectors|
|0x39||4||ceilingz||the closest ceiling level over all contacted sectors|
|0x3d||4||radius||initially copied from mobjinfo_t; changes to 0 when a monster is crushed|
|0x41||4||height||initially copied from mobjinfo_t; reduces when a monster dies, changes to 0 when crushed|
|0x45||4||momx||component of speed along the x axis in units per tic|
|0x49||4||momy||component of speed along the y axis in units per tic|
|0x4d||4||momz||component of speed along the z axis in units per tic|
|0x51||4||validcount||counter used to keep track of whether an mobj has already been touched during clipping operations|
|0x55||4||type||MT_* type (index into mobjinfo)|
|0x59||4||info||pointer to mobjinfo_t structure (same as &mobjinfo[mo->type])|
|0x5d||4||tics||tics left in current state; initially copied from state_t, counts down every call to P_MobjThinker|
|0x61||4||state||pointer to current state_t|
|0x65||4||flags||flags, initially copied from mobjinfo_t; may change at runtime in response to many different events|
|0x69||4||health||current health; can be zero or negative if the thing is dead.|
|0x6d||4||movedir||0-7, representing movement direction for monsters|
|0x71||4||movecount||when 0, a monster may select a new direction for movement|
|0x75||4||target||pointer to the current enemy (or the owner for projectiles). Zeroed when save is loaded because there is no pointer swizzling scheme. This results in monsters becoming dormant after restoring a game.|
|0x79||4||reactiontime||a delay before the next attack for monsters; delay until can move again when teleporting for players|
|0x7d||4||threshold||a threshold time before a monster will change targets due to being hit; does not apply to arch-viles|
|0x81||4||player||pointer to the player_t for player mobjs; NULL for any other type of object|
|0x85||4||lastlook||player number last looked for by monsters|
|0x89||4x5||spawnpoint||contents of spawn point mapthing_t structure, used for monster respawning|
|0x9d||4||tracer||thing being tracked by a homing missile; also used by arch-vile fire. Zeroed when loading saves, nullifying all tracking missiles and arch-vile spells in progress.|
To ensure consistency, the game expects the last byte of the savegame read into the buffer to be 0x1d. Otherwise it deems the file corrupt and bombs out with an error message.