# Save System

# Warning!

As of e1.0.10/e1.1.0 when defining custom data that inherits the game's basic types, be extra careful.
If the data will be saved to the game's internal storage, upon removing your mod the save won't be able to load because of your missing custom data.
This will be the case, for example, when definining custom LogEntry types and passing them to the game (LogEntry.AddLogEntry(customLog);).
As a workaround, you should either include a feature to fully remove your custom data from the game or provide a mod for it.

# Basic saving - SyncData

The core of saving is CampaignBehaviorBase's SyncData function. This will be called on any registered campaign behavior on both saving and loading.

The method is provided a DataStore object with a SyncData method, which takes a ref (opens new window) variable. On save, data from the ref is read and stored in the savefile, on load, the ref will be populated with the data from the save file.

public class CustomBehavior : CampaignBehaviorBase
{
    // The data in this field will persist across saving
    public string Data;

    public override void SyncData(IDataStore dataStore)
    {
        // First argument is an identifier, only needs to be unique to this behavior
        dataStore.SyncData("Data", ref Data);
    }

    public override void RegisterEvents() { }
}

# Advanced pattern - serialization/deserialization in SyncData

While it's most common to provide a ref to a class property, a ref can also be provided to a local variable, which can be used to specially serialize and deserialize data, alongside the IsLoading and IsSaving properties of the IDataStore.

public override void SyncData(IDataStore dataStore)
{
    var dataToSave = new List<string>();
    if(dataStore.IsSaving) {
        // Load the data to save into the list, can gather data from other parts of the mod
        dataToSave = gatherData();
    }
    dataStore.SyncData("dataToSave", ref dataToSave);
    if(dataStore.IsLoading) {
        // The save data has been loaded into dataToSave array.
        // Do whatever logic is necessary to restore the state based on the saved data
        restoreData(dataToSave);
    }
}

A more complete example of this is shown below in the JSON serialization section

# SaveableTypeDefiner

To save custom classes, structs, or built-in data structures (such as Array (opens new window), List (opens new window), Dictionary (opens new window), or Queue (opens new window)), implement your own TaleWorlds.SaveSystem.SaveableTypeDefiner, alongside the SyncData function. You don't need to register it somewhere, the game will find it itself via reflection.

# Saving custom classes with SaveableTypeDefiner

To make a custom class/struct saveable, register it inside DefineClassType in your SaveableTypeDefiner. To mark data in your custom class/struct as saveable, use TaleWorlds.SaveSystem.SaveableFieldAttribute and TaleWorlds.SaveSystem.SaveablePropertyAttribute. It seems that there is no real difference between SaveableField and SaveableProperty, but if you used one of them, stick to the type. They are not interchangeable and the data won't be loaded if types are switched. See the complete example below for examples of proper handling of SaveableField and SaveableProperty ids.

# Saving data structures with SaveableTypeDefiner

To save built-in data structures, register them in DefineContainerDefinitions. For example, if you want to save a list of ExampleStruct you would register:

protected override void DefineContainerDefinitions() {
    ConstructContainerDefinition(typeof(List<ExampleStruct>));
}

For nested structures, you'll need to register both the outer and inner structures. For a Dictionary of Lists of ExampleStruct, you would register:

protected override void DefineContainerDefinitions() {
    ConstructContainerDefinition(typeof(List<ExampleStruct>));
    ConstructContainerDefinition(typeof(Dictionary<List<ExampleStruct>>));
}

# Complete example with SaveableTypeDefiner

public class CustomBehavior : CampaignBehaviorBase
{
    public ExampleStruct StructData;
    public List<CustomMapNotification> MapNotificationList;
    public Dictionary<string, List<NestedStruct>> NestedData;

    public override void SyncData(IDataStore dataStore)
    {
        dataStore.SyncData("StructData", ref StructData);
        dataStore.SyncData("MapNotificationList", ref MapNotificationList);
        dataStore.SyncData("NestedData", ref NestedData);
    }

    public override void RegisterEvents() { }
}

public class CustomSaveDefiner : SaveableTypeDefiner
{
    // use a big number and ensure that no other mod is using a close range
    public CustomSaveDefiner() : base(2_333_000) { }

    protected override void DefineClassTypes()
    {
        // The Id's here are local and will be related to the Id passed to the constructor
        AddClassDefinition(typeof(CustomMapNotification), 1);
        AddStructDefinition(typeof(ExampleStruct), 2);
        AddClassDefinition(typeof(ExampleNested), 3);
        AddStructDefinition(typeof(NestedStruct), 4);
    }

    protected override void DefineContainerDefinitions()
    {
        ConstructContainerDefinition(typeof(List<CustomMapNotification>));
        // Both of these are necessary: order isn't important
        ConstructContainerDefinition(typeof(List<NestedStruct>));
        ConstructContainerDefinition(typeof(Dictionary<string, List<NestedStruct>>));
    }
}

public struct ExampleStruct
{
    // Local ID's start from one if the class/struct does not inherit from any
    // game's types with saveable data
    [SaveableField(1)]
    public PartyBase Attacker;
}

public class CustomMapNotification : InformationData
{
    // InformationData already contains 5 definitions, so start from 6 for custom data
    [SaveableProperty(6)]
    public Hero Mercenary { get; set; }

    [SaveableProperty(7)]
    public bool IsHandled { get; set; }
}

public struct NestedStruct
{
    [SaveableField(1)]
    public bool Flag;
}
public class ExampleNested
{
    [SaveableField(1)]
    public NestedStruct Data;
}

# Notes:

The community should decide how to handle saveBaseId collisions.

# Alternative approach - JSON serialization

Instead of relying on the SaveableTypeDefiner with it's necessity of pre-defining things like containers, the community found out another method of saving data, by using JSON serialization.

    public class CustomJSONBehavior : CampaignBehaviorBase
    {
        public class ExampleNested
        {
            public NestedStruct Data;
        }
        public struct NestedStruct
        {
            public bool Flag;
        }

        private List<ExampleNested> _customData = new List<ExampleNested>();

        public override void SyncData(IDataStore dataStore)
        {
            if (dataStore.IsSaving)
            {
                var jsonString = JsonConvert.SerializeObject(_customData);
                dataStore.SyncData("NestedStruct", ref jsonString);

            }
            if (dataStore.IsLoading)
            {
                var jsonString = "";
                if (dataStore.SyncData("NestedStruct", ref jsonString) && !string.IsNullOrEmpty(jsonString))
                {
                    _customData = JsonConvert.DeserializeObject<List<ExampleNested>>(jsonString);
                }
                else
                {
                    _customData = new List<ExampleNested>();
                }
            }
        }

        public override void RegisterEvents() { }
    }

# WARNING!

This approach should only used with custom objects, not in-game classes with an MBGUID property. Do not hold inside the custom objects any references to the in-game classes like Hero! They will be serialized wrong and this will cause issues after loading the save!

The game also has a hard limit on the string size. We don't know the exact maximum length, but it's something around short.MaxValue - 1024. If your final JSON string is bigger, we recommend splitting the string into string[] and using this for saving instead, here's an example for how you would do that:

        ...
        private static IEnumerable<string> ToChunks(string str, int maxChunkSize)
        {
            for (var i = 0; i < str.Length; i += maxChunkSize)
                yield return str.Substring(i, Math.Min(maxChunkSize, str.Length-i));
        }
        private static string ChunksToString(string[] chunks)
        {
            if (chunks.Length == 0)
                return string.Empty;
            else if (chunks.Length == 1)
                return chunks[0];

            var strBuilder = new StringBuilder(short.MaxValue);

            foreach (var chunk in chunks)
                strBuilder.Append(chunk);

            return strBuilder.ToString();
        }
        
        ...
        public override void SyncData(IDataStore dataStore)
        {
            if (dataStore.IsSaving)
            {
                var dataJson = JsonConvert.SerializeObject(_customData);
                var chunks = ToChunks(dataJson, short.MaxValue - 1024).ToArray();
                dataStore.SyncData("NestedStruct", ref chunks);
            }

            if (dataStore.IsLoading)
            {
                var jsonDataChunks = Array.Empty<string>();
                if (dataStore.SyncData("NestedStruct", ref jsonDataChunks) && jsonDataChunks is not null)
                {
                    JsonConvert.DeserializeObject<List<ExampleNested>>(ChunksToString(jsonDataChunks));
                }
                else
                {
                    _customData = new List<ExampleNested>();
                }
            }
        }