Librarianon

Your local Librarianon

  • He/Him

Writer, TF Finatic, Recohoster, and Game dev. Wasnt able to post here as much as I liked, but I'll miss it and all of yall. Till we meet again, friends!


eniko
@eniko

hello, little gamedev pro tip from me to you: always put a version number in any file your game saves externally. this includes save files but it also comes in real handy in config files since if you change the way your game's configuration work you can use it to invalidate or upgrade a user's previous config!


mrhands
@mrhands

If there's one neat trick I've picked up from the Amazon© Lumberyard™ engine, a horrible product that has thankfully been discontinued, it's the idea of an upgrade() method for your serializable data. As the extremely experienced Ms. Fox rightfully points out, you should always put a version number in your data. With this version number in hand, you can (theoretically) upgrade all user data to the latest version without ever losing data!

For example, my game had a TaskDataModel struct that needed to be loaded from the save file. In version 1, the task had an "alignment" stored as an integer value. But for my own sanity, I wanted to change this to a string value in the save file. So, I wrote an upgrade function for the struct:

template <>
inline bool upgrade(TaskDataModel& to, QJsonObject& data, int32_t dataVersion, Mode mode)
{
	// ChangedAlignmentIdentifier

	if (dataVersion < TaskDataModel::Version::ChangedAlignmentIdentifier)
	{
		if (data.find("alignment") != data.end())
		{
			int32_t alignment = data["alignment"].toInt();
			if (alignment >= 0 &&
				alignment <= 2)
			{
				const char* alignment_ids[] = { "nerd", "cool", "jock" };
				data["alignment"] = alignment_ids[alignment];
			}
			else
			{
				data["alignment"] = "";
			}
		}
		else
		{
			data["alignment"] = "";
		}
	}

	return true;
}

This function reads the old data, extracts the alignment as an integer, and then writes the new alignment as a string value. The new serialization code can then always assume that the alignment is read as a string.

If the incoming struct already has the latest version, we don't need to call this method at all, and this is handled at a higher level of my serialization system. But crucially, we can upgrade from version 1 to version 24 by running each step in sequence! The real version of this function has these steps:

template <>
inline bool upgrade(TaskDataModel& to, QJsonObject& data, int32_t dataVersion, Mode mode)
{
	// ChangedAlignmentIdentifier

	// AddedPosition

	// ChangedSuccessToDifficulty

	// RebalancedDifficulty

	// ChangedLocationIdentifierToEnum

	// RenamedPhaseToShift

	// RenamedPhaseToShift

	// MovedIdentifierToSheet

	// ChangedDiceSlotsToDataModelList

	return true;
}

If I ever publish my game (ha!), this system means that as long as I keep incrementing the version number for the structs, I can continuously make patches while keeping my players' save files intact. 😊


You must log in to comment.

in reply to @eniko's post:

in reply to @mrhands's post:

Absolutely did this for 1000xRESIST, a game that we expected to do basically zero post-launch support for because you never know. Picked up the habit while working on mobile live service games.