Skip to content

Savegame format

Nightinggale edited this page Sep 11, 2019 · 1 revision

Savegame Format

What's wrong with the vanilla format?

It's fairly primitive. What it does is basically dump memory contents to the file and on load it will read the same data and assume the order of the bytes to be the same.

The problem with this approach is that it's up to the programmer to ensure that the order is the same and that the game assumes xml data to be unchanged. This means the savegame format is fragile and can be broken by modding either the DLL or xml and the modder might not even have to touch the savegame code itself. Also adding or removing data in savegames without breaking existing savegames quickly results in messy code, which is prone to be buggy.

The savegame code needs to be bug free because any bug can render all savegames corrupt.

How is the new format better

Variable order doesn't matter

Each variable in each class has an enum value assigned. Whenever a variable is saved, start by saving the enum. On read, the enum is read and then a switch case will be used to read the various variables, be it single variables, arrays, classes etc. This completely eliminates the bug where read and write code goes out of sync due to a human programming error.

Each variable has a default value

When loading a class, first assign a default value to all the variables (more or less the same as vanilla reset). If a variable isn't mentioned in the savegame, then the variable will have the default value. This means if somebody adds a new variable and an old savegame is loaded, no extra loading code is needed. The variable will just have the default value (whatever it is).

A nice bonus in this is that not saving a variable is the same as saving the default value. This means if a variable has the default value, the savegame code will now skip the variable and not write it at all to the file. Many variables are left on the default value most of the time. Think unit experience. Civilian units will not gain combat experience meaning with this approach all civilian units will skip saving experience entirely. This has a positive impact on the savegame size.

XML data is converted on load if needed

The very start of the savegame contains lists of Type from various xml files. On load, the savegame types and the currently loaded types are compared and a conversion table is set up. When a variable is read, which is supposed to be a reference to an xml index, the new value is looked up in the table. The programmer doesn't really have to trigger the conversion as it's done automatically with function overloading. If the variable is a BonusTypes, it will automatically use the bonus index conversion without any extra code. If the bonus is no longer present, then it will read -1 (NO_BONUS) meaning it will just remove the bonuses from the map and then keep on playing.

Arrays are handled in a similar way. The savegame code has no "save array of 20 ints" function. Instead it is intended to be used with JustInTimeArray. If it reads say a YieldArray (one of the arrays, which inherits the JustInTimeArray base class), then the read code knows each index is a reference to indexes in the yield xml file and it will convert each index on load. New yields will get the default value in the array and removed yields will be read, but not saved in memory (hence discarded).

Compression friendly

Savegames are stored in memory as byte arrays and the interaction with the exe is done with a single array read/write. This way the savegame can be compressed to reduce the file size.

How to code for the new format

The easiest way is to look at CvMap as this class has the new savegame structure. The interesting parts are read(CvSavegameReader&) and Write(CvSavegameWriter&).

Reader

A switch case setup means there is one case for each variable. It's usually done with reader.Read(variable&) and that's it. This will result in lines like:

case Save_GridWidth:           reader.Read(m_iGridWidth);          break;

Use spaces to align reader, break etc as this makes the file much easier to read. Most of the time function overloading means that's all it takes to read data (JustInTimeArrays included), but sometimes it's more complex to read something and then multiple lines will be needed. A good example is:

		case Save_Plots:
		{
			FAssertMsg(m_pMapPlots == NULL, "Memory leak");
			int iNumPlots = numPlotsINLINE();
			m_pMapPlots = new CvPlot[iNumPlots];
			for (int iI = 0; iI < iNumPlots; ++iI)
			{
				m_pMapPlots[iI].read(reader);
			}
			break;
		}

Writer

This mainly consist of a setup like this:

writer.Write(Save_GridWidth, m_iGridWidth, defaultGridWidth);

First is the enum value, the next is the variable and the last is the default value, which can be used to skip saving the variable entirely. If it's a JustInTimeArray, then the default is skipped because JustInTimeArrays contains a default value.

More complex data can also be written like this:

	if (numPlotsINLINE() > 0)
	{
		writer.Write(Save_Plots);
		int iNumPlots = numPlotsINLINE();
		for (int iI = 0; iI < iNumPlots; iI++)
		{
			m_pMapPlots[iI].write(writer);
		}
	}

When making a custom complex variable, do remember to always start with the enum value as this is assumed by the switch-case to trigger the read code.

Adding more savegame data

You need a new enum value for your new variable. Add it to the end. NEVER CHANGE EXISTING ENUM VALUES!!! Doing so will break savegames. Just extend the list. We have 16 bit of values available and should never run out.

Add read and write code. The order doesn't matter.

Removing no longer needed data

Deleting the write code is trivial. The read code should however stay as it is required to read the bytes in the savegame. Just read and discard. For instance if it's an int, then make a temp int and read into that. You don't have to use the contents of the temp variable for anything if it's not needed. All the savegame cares about is that you read the same amount of bytes as was saved, which will move the savegame forward to the next enum for the next read iteration.

Don't save xml data or caches

Vanilla has a tendency to save cached data, which is bad, particularly if the cache is based on xml data. Take for instance unit movement points. By saving those you get the movement points at the time the unit was created. If you do not save the movement points, but instead calculate them based on unit xml data and promotions after the data is loaded, then a change to movement in xml will apply to existing savegames on load.

While it's often a bad idea to save caches, sometimes it will make sense. A review of each specific case is needed and it's something, which is open to discussions. If you aren't sure, ask other developers.