What is a good way to implement Saving System with implementation updates?

Hello guys! I have been using the Saving System from the RPG Course and have some questions regarding it.

In my case with CaptureState(), I initially saved the following data:
List<Attribute> attributes;

However, as development progressed, there were changes in implementation and it was changed to:
Attribute[] attributes;

Loading the old save file that had saved List<Attribute> data and restoring it with RestoreState() to Attribute[] data gave a runtime error. During development, the solution to this was to delete the existing save file with the old data implementation in order for the Saving System to start saving using the new data implementation with a fresh start. However, when a game is released or launched, we surely would want the previously saved data to be retrieved and incorporated with the new implementation brought by game updates.

With this, I would like to ask, what do you guys think would be a great or best way to address this problem without having the need to delete the existing save file? Do you guys have any techniques you could share on how most complex games do their saving with updates/patches in mind?

Thanks in advance to you guys that could share something about this!

The easy answer; don’t change the way things are saved once it’s released.

The harder answer; Implement a versioning system. Load from older versions, but always save as the newest version. That way you ‘upgrade’ the save without the need to delete it.

To version you can have a entry at the same place where you save the last scene. Perhaps also change the ISaveable interface to pass not only the data, but the version of the save to RestoreState. Then, if you need to, you can check the version and load the data based on the version that you received. Something like

void RestoreState(int version, object data)
{
    if (version == 0)
    {
        attributes = ((List<Attribute>)data).ToArray();
    }
    else
    {
        attributes = (Attribute[])data;
    }
}

Now, if you change again, it will be another version

void RestoreState(int version, object data)
{
    if (version == 0)
    {
        attributes = new AttributesList((List<Attribute>)data);
    }
    else if (version == 1)
    {
        attributes = new AttributesList((Attribute[])data);
    }
    else
    {
        attributes = (AttributesList)data;
    }
}

This is just a quick example. You’d probably not use all the 'if’s but you get the idea

1 Like

@bixarrio Thank you very much for your suggestion! I will look into implementing a Versioning System for this.

Just my curiosity though. Is it correct for me to assume that every component that implements ISaveable would have different version checks depending on when their specific change was implemented? For example in Attributes, it would have version checks for Version 0 and Version 1, but say, Weapons would have checking for Version 0, Version 3 and Version 5. Or would it be better for all components to have a uniformed version checking?

You would only care about the versions where things changed. If the weapons save changed at version 3, you’d do the condition differently, but you’d only care about the change. If nothing changed, you don’t need to look at the version

void RestoreState(int version, object data)
{
    if (version == 0 || version == 1 || version == 2) { /* load this way */ }
    else if (version == 3 || version == 4) { /* load that way */ }
    else { /* load current way */ }
}

You could also make a dictionary with ‘load handlers’ if you get to a lot of versions

private Dictionary<int, Action<object>> loadHandlers = default;

void RestoreState(int version, object data)
{
    InitLoadHandlers();
    loadHandlers[version].Invoke(data);
}

void LoadDataV0(object data) => attributes = new AttributeList((List<Attribute>)data);
void LoadDataV3(object data) => attributes = new AttributeList((Attribute[])data);
void LoadDataV5(object data) => attributes = (AttributeList)data;

void InitLoadHandlers()
{
    if (loadHandlers != null) return;
    loadHandlers = new Dicstionary<int, Action<object>>();
    loadHandlers.Add(0, LoadDataV0);
    loadHandlers.Add(1, LoadDataV0);
    loadHandlers.Add(2, LoadDataV0);
    loadHandlers.Add(3, LoadDataV3);
    loadHandlers.Add(4, LoadDataV3);
    loadHandlers.Add(5, LoadDataV5);
}

This will pick the correct handler from the dictionary and use it to load the data. This means that as soon as there’s one version change, you will have to update it for all versions hence forth… Perhaps not the best option. Let me think about it a bit.

I was going to suggest dedicated components for each version, but then I remembered that you would need that for each existing component and that could get very messy.

@bixarrio Thanks a lot for confirming and giving more suggestions!

I think the Load Handlers part will be very helpful in making the system more readable. I’ll study your implementations and give it a try.

I really appreciate your help on this!

The handlers is cleaner, but it has issues. With it you will have to have a handler in the dictionary for each version. You can reduce the registration with a binary search but that only works if they have been registered in ascending order. They have to be in ascending order for the binary search to work

private Dictionary<int, Action<object>> loadHandlers = default;

void RestoreState(int version, object data)
{
    InitLoadHandlers();
    FindBestHandler(version).Invoke(data);
}

void LoadDataV0(object data) => attributes = new AttributeList((List<Attribute>)data);
void LoadDataV3(object data) => attributes = new AttributeList((Attribute[])data);
void LoadDataV5(object data) => attributes = (AttributeList)data;

void InitLoadHandlers()
{
    if (loadHandlers != null) return;
    loadHandlers = new Dicstionary<int, Action<object>>();
    loadHandlers.Add(0, LoadDataV0);
    loadHandlers.Add(3, LoadDataV3);
    loadHandlers.Add(5, LoadDataV5);
}

Action<object> FindBestHandler(int version)
{
    var keys = loadHandlers.Keys.ToList();
    var best = keys.BinarySearch(version);
    if (best < 0) best = ~best - 1;
    return loadHandlers[keys[best]];
}

The above will return LoadDataV0 if the version is 0, 1 or 2, LoadDataV3 if the version is 3 or 4, and LoadDataV5 if it’s 5 or above.

But now you need one of these for each component that wants to save stuff with versioning. FindBestHandler could probably be in some extension method or other common utilities class somewhere so you don’t have to duplicate it everywhere


Edit
The binary search will break if there is no version 0 and the save file is saved with version 0. so if you introduced a new save for anything at a later version, you’d have to register its first save as 0.

@bixarrio Ohhh, I see. I’ll take note of these important points. Thank you very much!

I’m going to demonstrate a simpler, cleaner way of handling versioning, without worrying about versioning numbers (which are notoriously easy to forget to update!).

public void RestoreState(object state)
{
    if(state is List<Attribute> attributeList) attributes = attributeList.ToArray(); //using System.Linq;
    else if(state is Attribute[] attributeArray) attributes = attributeArray;
}
2 Likes

Hello @Brian_Trotter. Thanks for this suggestion! I’m also trying to consider this seeing as how simple it would be and less prone to forgetting to update version numbers.

However, I also have some curiosities. Say for example, in the case that the team changes the implementation to a Dictionary, would this mean that they would need another if-statement to support this?

If that case would happen in the future, my assumption would be, the current implementation of:

public void RestoreState(object state)
{
    if(state is List<Attribute> attributeList) attributes = attributeList.ToArray(); //using System.Linq;
    else if(state is Attribute[] attributeArray) attributes = attributeArray;
}

would be updated to:

public void RestoreState(object state)
{
    if (state is List<Attribute> attributeList) 
    {
         // Do code to convert List to Dictionary
    } 
    else if (state is Attribute[] attributeArray)
    {
        // Do code to convert Array to Dictionary
    }
    else if (/* state is already a Dictionary */)
    {
        // No data conversion needed, set immediately to restore data
    }
}

With this, I’m assuming that every time there is a change in implementation, each case (considering that Players played the game during different states/updates of the game) would also need to be updated to support the latest implementation. Is my assumption correct? And if so, I would like to ask your opinion if this is okay or if this would still be an efficient way, especially regarding refactoring efforts in the future?

Thanks in advance for helping out! Still learning though and have been so curious :grinning_face_with_smiling_eyes:

Brian’s solution is the same as mine with the only difference being that we check the type of the data returned from the save system as opposed to a version number. Everything else is the same. Any considerations you had to make for my suggestion is also applicable to Brian’s.

1 Like

Absolutely. Either by versioning or by testing against is, you need to be able to safely decode the data.

One optimization you can make to my version is to reverse the order so that you test for the most recent data format first, and then test for the fallbacks.

An important note here… If you’re using the JsonSavingSystem, the is keyword won’t work. For that, you’ll need to either add in a version number or use JObjects out of the gate when you think you may need updating in the future. Actually, in the Json system, you would likely already be saving the attributes as a JObject.

public JToken CaptureAsJToken()
{
    JObject state = new JObject;
    IDictionary<string, JToken> stateDict = state;
    for(int i=0;i<attributes.Length; i++)
    {
         //Convert one Attribute to a JToken
         stateDict[$"{i}"] = attributeAsToken;
    }
    return state;
}

Now in this case, decoding the data as an array or List will be exactly the same, so no version provision would be needed…

public void RestoreFromJToken(JToken state)
{
    JObject stateObj = (JObject)state;
    IDictionary<string, JToken> stateDict = stateObj;
     List<Attribute> result = new List<Attribute>();
    foreach(JToken token in stateDict.Values())
    {
         //Convert the token to an Attribute
          result.Add(convertedAttribute);
    }
    // attributes = result; //If still List
    attributes = result.ToArray(); //If now array
}

Then if it’s now a Dictionary, add a key for “isDictionary”, doesn’t matter the value. Test for if(stateDict.ContainsKey(“isDictionary”) and decode it as a Dictionary instead of as a list.

1 Like

@bixarrio @Brian_Trotter Thank you very much to you both! I’m considering very well the suggestions you guys have shared. Really appreciate it!

Privacy & Terms