Versioning and Migrating Game Save Files

In this post I briefly described how I handle saving the game state in a JSON file. One thing that I knew I had to address is migration of game save files from one version of the game to the next. During alpha and beta it may not be as important to be forward compatible, but when the game is released I’ll have to accommodate for this. Otherwise if any of the models in the game change, an old game save file may load in an inconsistent state, or worse, fail to load altogether.

I put together a simple migration mechanism that allows me to directly manipulate the JSON before it’s deserialized into the game entities/objects. This is not too dissimilar to many libraries used for migrating databases:

The Migration

A migration is identified with an interface that has two members:


public interface IMigration
{
  int TargetVersion { get; }
  void Migrate(JObject source);
}

The TargetVersion is the version of the game save file after the the migration has executed. This may or may not match with the version of the game itself. This number is stored in the save files and, as we will see, is important in figuring out which migration(s) we need to execute for a given save file. There are other ways to handle this. I could rely on the migration file name, for example, however I decided to make this an explicit number. The downside is that you have to be careful to version your migrations correctly. Using duplicate numbers will almost certainly cause disaster. If you are really worried you can probably write a convention test to make sure this holds.

The Migrate() method does the bulk of the work. It takes a JObject (see Json.NET) and modifies it as needed. Functional programmers will cringe at this, as the migrate method is directly mutating the state of the JObject rather than returning a new (modified) state. However this is the easiest way to work with the Json.NET interface and I didn’t want to fight it! Alright? Get off my back… ehem

To make the implementation of migration easier, I wrote a base class with some common functionality:


public abstract class MigrationBase : IMigration
{
  protected MigrationBase(int targetVersion)
  {
    TargetVersion = targetVersion;
  }

  public int TargetVersion { get; private set; }
  protected int StartId
  {
    get { return (TargetVersion + 1) * 10000; }
  }

  public abstract void Migrate(JObject source);
  ...
}

There are several additional utility methods (not shown here) for retrieving common parts of the JSON object tree from the source. These will be specific to your project. You’ll also note the StartId property which essentially allocates an ID space for each migration so new JSON objects can be created without collision.

Here is an example implementation:


public class MigrationV5 : MigrationBase
{
  public MigrationV5() : base(5)
  {
  }

  public override void Migrate(JObject source)
  {
    AddMilestoneC(source);
  }

  void AddMilestoneC(JObject source)
  {
    // implemented in base class
    var milestoneB = GetMilestoneCheckpoints(source, 3);

    var milestoneC = new JArray(new JObject
    {
      {"$id", StartId},
      {"title", "Sell your company"},
      {"startValue", "0.0"},
      {"endValue", "1.0"},
      {"currentValue", 0},
      {"pointSize", "1.0"},
      {"totalPoints", "1"}
    });

    milestoneB.AddAfterSelf(milestoneC);
  }
}

The Migrator

Great, we have migrations. But how do we actually execute them? With the migrator class of course:


class SaveFileMigrator : ISaveFileMigrator
{
  readonly List<IMigration> _migrations;

  public SaveFileMigrator(List<IMigration> migrations)
  {
    _migrations = migrations;
  }

  public int GameSaveVersion
  {
    get { return _migrations.Max(m => m.TargetVersion); }
  }

  public string Migrate(string sourceJson)
  {
    var model = JObject.Parse(sourceJson);
    JToken versionToken;
    var hasVersion = model.TryGetValue("version", out versionToken);
    var saveFileVersion = hasVersion
      ? versionToken.ToObject<int>()
      : -1;

    _migrations
      .OrderBy(m => m.TargetVersion)
      .SkipWhile(m => saveFileVersion >= m.TargetVersion)
      .ToList()
      .ForEach(m => m.Migrate(model));

    return model.ToString();
  }
}

No rocket science going on here: we pass a list of all migration in our project to the migrator. When we want to migrate, the full JSON from the save file is passed to the Migrate() method. We try to extract the version number from it. We then skip all migration that have an older version number, and finally apply the remaining migrations (in ascending version order.

Assembly Scanning

Please don’t manually create the list of migrations to pass to the migrator. Instead automate it by assembling scanning (I actually register these with my IoC container):


var migrations = typeof(IMigration).Assembly
  .GetTypes()
  .Where(t => !t.IsGenericType)
  .Where(t => !t.IsAbstract)
  .Where(t => t.GetInterfaces().Any(x => x == typeof(IMigration)))
  .ToList();
  ...

I hope this was useful to some extent. I have tried it for a handful of migrations and it works quite well. Having said that, mileage may vary depending on the extent and complexity of the changes to your entities. In extreme cases it may be necessary to actually version you C# classes to allow you more robust migrations. You can still use the mechanism above for that. Then again with a change that drastic, you may be better off just asking for the player’s forgiveness and not migrate their save files.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s