Skip to content

Commit

Permalink
Merge pull request #270 from Falki-git/plugin-data-in-save-files
Browse files Browse the repository at this point in the history
Add API registration for mods that want to use game's built-in saving and loading of data stored in the save game files
  • Loading branch information
cheese3660 authored Nov 7, 2023
2 parents 6338e33 + 219d8ff commit ea55371
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 0 deletions.
88 changes: 88 additions & 0 deletions SpaceWarp.Core/API/SaveGameManager/ModSaves.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using JetBrains.Annotations;
using SpaceWarp.Backend.SaveGameManager;
using System;
using System.Collections.Generic;
using SpaceWarp.API.Logging;

namespace SpaceWarp.API.SaveGameManager;

[PublicAPI]
public static class ModSaves
{
private static readonly ILogger _logger = new UnityLogSource("SpaceWarp.ModSaves");

internal static List<PluginSaveData> InternalPluginSaveData = new();

/// <summary>
/// Registers your mod data for saving and loading events.
/// </summary>
/// <typeparam name="T">Any object</typeparam>
/// <param name="modGuid">Your mod GUID. Or, technically, any kind of string can be passed here, but what is mandatory is that it's unique compared to what other mods will use.</param>
/// <param name="onSave">Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback.</param>
/// <param name="onLoad">Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback.</param>
/// <param name="saveData">Your object that will be saved to a save file during a save event and that will be updated when a load event pulls new data. Ensure that a new instance of this object is NOT created after registration.</param>
/// <returns>T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything</returns>
public static T RegisterSaveLoadGameData<T>(string modGuid, Action<T> onSave, Action<T> onLoad, T saveData = default)
{
// Create adapter functions to convert Action<T> to CallbackFunctionDelegate
SaveGameCallbackFunctionDelegate saveCallbackAdapter = (object saveData) =>
{
if (onSave != null && saveData is T data)
{
onSave(data);
}
};

SaveGameCallbackFunctionDelegate loadCallbackAdapter = (object saveData) =>
{
if (onLoad != null && saveData is T data)
{
onLoad(data);
}
};

// Check if this GUID is already registered
if (InternalPluginSaveData.Find(p => p.ModGuid == modGuid) != null)
{
throw new ArgumentException($"Mod GUID '{modGuid}' is already registered. Skipping.", "modGuid");
}
else
{
if (saveData == null)
saveData = Activator.CreateInstance<T>();

InternalPluginSaveData.Add(new PluginSaveData { ModGuid = modGuid, SaveEventCallback = saveCallbackAdapter, LoadEventCallback = loadCallbackAdapter, SaveData = saveData });
_logger.LogInfo($"Registered '{modGuid}' for save/load events.");
return saveData;
}
}

/// <summary>
/// Unregister your previously registered mod data for saving and loading. Use this if you no longer need your data to be saved and loaded.
/// </summary>
/// <param name="modGuid">Your mod GUID you used when registering.</param>
public static void UnRegisterSaveLoadGameData(string modGuid)
{
var toRemove = InternalPluginSaveData.Find(p => p.ModGuid == modGuid);
if (toRemove != null)
{
InternalPluginSaveData.Remove(toRemove);
_logger.LogInfo($"Unregistered '{modGuid}' for save/load events.");
}
}

/// <summary>
/// Unregisters then again registers your mod data for saving and loading events
/// </summary>
/// <typeparam name="T"> Any object</typeparam>
/// <param name="modGuid">Your mod GUID. Or, technically, any kind of string can be passed here, but what is mandatory is that it's unique compared to what other mods will use.</param>
/// <param name="onSave">Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback.</param>
/// <param name="onLoad">Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback.</param>
/// <param name="saveData">Your object that will be saved to a save file during a save event and that will be updated when a load event pulls new data. Ensure that a new instance of this object is NOT created after registration.</param>
/// <returns>T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything</returns>
public static T ReregisterSaveLoadGameData<T>(string modGuid, Action<T> onSave, Action<T> onLoad, T saveData = default(T))
{
UnRegisterSaveLoadGameData(modGuid);
return RegisterSaveLoadGameData<T>(modGuid, onSave, onLoad, saveData);
}
}
17 changes: 17 additions & 0 deletions SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace SpaceWarp.Backend.SaveGameManager;

internal delegate void SaveGameCallbackFunctionDelegate(object data);

[Serializable]
public class PluginSaveData
{
public string ModGuid { get; set; }
public object SaveData { get; set; }

[NonSerialized]
internal SaveGameCallbackFunctionDelegate SaveEventCallback;
[NonSerialized]
internal SaveGameCallbackFunctionDelegate LoadEventCallback;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;

namespace SpaceWarp.Backend.SaveGameManager;

/// <summary>
/// Extension of game's save/load data class
/// </summary>
[Serializable]
public class SpaceWarpSerializedSavedGame : KSP.Sim.SerializedSavedGame
{
public List<PluginSaveData> SerializedPluginSaveData = new();
}
23 changes: 23 additions & 0 deletions SpaceWarp.Core/InternalUtilities/InternalExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Reflection;
using System;
using UnityEngine;

namespace SpaceWarp.InternalUtilities;
Expand All @@ -9,4 +11,25 @@ internal static void Persist(this UnityObject obj)
UnityObject.DontDestroyOnLoad(obj);
obj.hideFlags |= HideFlags.HideAndDontSave;
}

internal static void CopyFieldAndPropertyDataFromSourceToTargetObject(object source, object target)
{
foreach (FieldInfo field in source.GetType().GetFields())
{
object value = field.GetValue(source);

try
{
field.SetValue(target, value);
}
catch (FieldAccessException)
{ /* some fields are constants */ }
}

foreach (PropertyInfo property in source.GetType().GetProperties())
{
object value = property.GetValue(source);
property.SetValue(target, value);
}
}
}
89 changes: 89 additions & 0 deletions SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using HarmonyLib;
using KSP.Game.Load;
using KSP.IO;
using SpaceWarp.API.Logging;
using SpaceWarp.API.SaveGameManager;
using SpaceWarp.Backend.SaveGameManager;
using SpaceWarp.InternalUtilities;
using System;

namespace SpaceWarp.Patching.SaveGameManager;

[HarmonyPatch]
internal class SaveLoadPatches
{
private static readonly ILogger _logger = new UnityLogSource("SpaceWarp.SaveLoadPatches");

/// SAVING ///

[HarmonyPatch(typeof(SerializeGameDataFlowAction), MethodType.Constructor), HarmonyPostfix]
[HarmonyPatch(new Type[] { typeof(string), typeof(LoadGameData) })]
private static void InjectPluginSaveGameData(string filename, LoadGameData data, SerializeGameDataFlowAction __instance)
{
// Skip plugin data injection if there are no mods that have registered for save/load actions
if (ModSaves.InternalPluginSaveData.Count == 0)
return;

// Take the game's LoadGameData, extend it with our own class and copy plugin save data to it
SpaceWarpSerializedSavedGame modSaveData = new();
InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(data.SavedGame, modSaveData);
modSaveData.SerializedPluginSaveData = ModSaves.InternalPluginSaveData;
data.SavedGame = modSaveData;

// Initiate save callback for plugins that specified a callback function
foreach (var plugin in ModSaves.InternalPluginSaveData)
{
plugin.SaveEventCallback(plugin.SaveData);
}
}

/// LOADING ///

[HarmonyPatch(typeof(DeserializeContentsFlowAction), "DoAction"), HarmonyPrefix]
private static bool DeserializeLoadedPluginData(Action resolve, Action<string> reject, DeserializeContentsFlowAction __instance)
{
// Skip plugin deserialization if there are no mods that have registered for save/load actions
if (ModSaves.InternalPluginSaveData.Count == 0)
return true;

__instance._game.UI.SetLoadingBarText(__instance.Description);
try
{
// Deserialize save data to our own class that extends game's SerializedSavedGame
SpaceWarpSerializedSavedGame serializedSavedGame = new();
IOProvider.FromJsonFile<SpaceWarpSerializedSavedGame>(__instance._filename, out serializedSavedGame);
__instance._data.SavedGame = serializedSavedGame;
__instance._data.DataLength = IOProvider.GetFileSize(__instance._filename);

// Perform plugin load data if plugin data is found in the save file
if (serializedSavedGame.SerializedPluginSaveData.Count > 0)
{
// Iterate through each plugin
foreach (var loadedData in serializedSavedGame.SerializedPluginSaveData)
{
// Match registered plugin GUID with the GUID found in the save file
var existingData = ModSaves.InternalPluginSaveData.Find(p => p.ModGuid == loadedData.ModGuid);
if (existingData == null)
{
_logger.LogWarning($"Saved data for plugin '{loadedData.ModGuid}' found during a load event, however that plugin isn't registered for save/load events. Skipping load for this plugin.");
continue;
}

// Perform a callback if plugin specified a callback function. This is done before plugin data is actually updated.
existingData.LoadEventCallback(loadedData.SaveData);

// Copy loaded data to the SaveData object plugin registered
InternalExtensions.CopyFieldAndPropertyDataFromSourceToTargetObject(loadedData.SaveData, existingData.SaveData);
}
}
}
catch (Exception ex)
{
UnityEngine.Debug.LogException(ex);
reject(ex.Message);
}
resolve();

return false;
}
}

0 comments on commit ea55371

Please sign in to comment.