From daf1d7543876f2ea0de5f0e96e32f9f2aaeae95b Mon Sep 17 00:00:00 2001 From: Falki-git <72734856+Falki-git@users.noreply.github.com> Date: Sun, 5 Nov 2023 21:47:59 +0100 Subject: [PATCH 1/3] Add API registration for mods that want to use game's built-in saving and loading of data stored in the save game files --- .../API/SaveGameManager/ModSaves.cs | 82 +++++++++++++++++ .../Backend/SaveGameManager/PluginSaveData.cs | 17 ++++ .../SpaceWarpSerializedSavedGame.cs | 13 +++ .../InternalUtilities/InternalExtensions.cs | 23 +++++ .../SaveGameManager/SaveGamePatches.cs | 89 +++++++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 SpaceWarp.Core/API/SaveGameManager/ModSaves.cs create mode 100644 SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs create mode 100644 SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs create mode 100644 SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs diff --git a/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs new file mode 100644 index 00000000..f6597011 --- /dev/null +++ b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs @@ -0,0 +1,82 @@ +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 InternalPluginSaveData = new(); + + /// + /// Registers your mod data for saving and loading events. + /// + /// + /// 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. + /// 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. + /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. + /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. + public static void RegisterSaveLoadGameData(string modGuid, T saveData, Action saveEventCallback, Action loadEventCallback) + { + // Create adapter functions to convert Action to CallbackFunctionDelegate + SaveGameCallbackFunctionDelegate saveCallbackAdapter = (object saveData) => + { + if (saveEventCallback != null && saveData is T data) + { + saveEventCallback(data); + } + }; + + SaveGameCallbackFunctionDelegate loadCallbackAdapter = (object saveData) => + { + if (loadEventCallback != null && saveData is T data) + { + loadEventCallback(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 + { + InternalPluginSaveData.Add(new PluginSaveData { ModGuid = modGuid, SaveData = saveData, SaveEventCallback = saveCallbackAdapter, LoadEventCallback = loadCallbackAdapter }); + _logger.LogInfo($"Registered '{modGuid}' for save/load events."); + } + } + + /// + /// Unregister your previously registered mod data for saving and loading. Use this if you no longer need your data to be saved and loaded. + /// + /// Your mod GUID you used when registering. + 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."); + } + } + + /// + /// Unregisters then again registers your mod data for saving and loading events + /// + /// + /// 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. + /// 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. + /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. + /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. + public static void ReregisterSaveLoadGameData(string modGuid, T saveData, Action saveEventCallback, Action loadEventCallback) + { + UnRegisterSaveLoadGameData(modGuid); + RegisterSaveLoadGameData(modGuid, saveData, saveEventCallback, loadEventCallback); + } +} diff --git a/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs b/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs new file mode 100644 index 00000000..768e5bf3 --- /dev/null +++ b/SpaceWarp.Core/Backend/SaveGameManager/PluginSaveData.cs @@ -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; +} diff --git a/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs b/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs new file mode 100644 index 00000000..2ec048d0 --- /dev/null +++ b/SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace SpaceWarp.Backend.SaveGameManager; + +/// +/// Extension of game's save/load data class +/// +[Serializable] +public class SpaceWarpSerializedSavedGame : KSP.Sim.SerializedSavedGame +{ + public List SerializedPluginSaveData = new(); +} diff --git a/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs index 46db62f8..0e953df8 100644 --- a/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs +++ b/SpaceWarp.Core/InternalUtilities/InternalExtensions.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using System; using UnityEngine; namespace SpaceWarp.InternalUtilities; @@ -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); + } + } } \ No newline at end of file diff --git a/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs new file mode 100644 index 00000000..49fcbc4f --- /dev/null +++ b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs @@ -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 not 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 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(__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; + } +} From f74ad7c93545f05edb8bc0a58cb354a0f9170728 Mon Sep 17 00:00:00 2001 From: Falki-git <72734856+Falki-git@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:46:47 +0100 Subject: [PATCH 2/3] Registration returns T; T saveData is optional, default value of T is created if parameter is omitted --- .../API/SaveGameManager/ModSaves.cs | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs index f6597011..0c98189e 100644 --- a/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs +++ b/SpaceWarp.Core/API/SaveGameManager/ModSaves.cs @@ -16,27 +16,28 @@ public static class ModSaves /// /// Registers your mod data for saving and loading events. /// - /// - /// 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. + /// Any object + /// 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. + /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. + /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. /// 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. - /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. - /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. - public static void RegisterSaveLoadGameData(string modGuid, T saveData, Action saveEventCallback, Action loadEventCallback) + /// T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything + public static T RegisterSaveLoadGameData(string modGuid, Action onSave, Action onLoad, T saveData = default) { // Create adapter functions to convert Action to CallbackFunctionDelegate SaveGameCallbackFunctionDelegate saveCallbackAdapter = (object saveData) => { - if (saveEventCallback != null && saveData is T data) + if (onSave != null && saveData is T data) { - saveEventCallback(data); + onSave(data); } }; SaveGameCallbackFunctionDelegate loadCallbackAdapter = (object saveData) => { - if (loadEventCallback != null && saveData is T data) + if (onLoad != null && saveData is T data) { - loadEventCallback(data); + onLoad(data); } }; @@ -47,8 +48,12 @@ public static void RegisterSaveLoadGameData(string modGuid, T saveData, Actio } else { - InternalPluginSaveData.Add(new PluginSaveData { ModGuid = modGuid, SaveData = saveData, SaveEventCallback = saveCallbackAdapter, LoadEventCallback = loadCallbackAdapter }); + if (saveData == null) + saveData = Activator.CreateInstance(); + + InternalPluginSaveData.Add(new PluginSaveData { ModGuid = modGuid, SaveEventCallback = saveCallbackAdapter, LoadEventCallback = loadCallbackAdapter, SaveData = saveData }); _logger.LogInfo($"Registered '{modGuid}' for save/load events."); + return saveData; } } @@ -69,14 +74,15 @@ public static void UnRegisterSaveLoadGameData(string modGuid) /// /// Unregisters then again registers your mod data for saving and loading events /// - /// - /// 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. + /// Any object + /// 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. + /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. + /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. /// 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. - /// Function that will execute when a SAVE event is triggered. 'NULL' is also valid here if you don't need a callback. - /// Function that will execute when a LOAD event is triggered. 'NULL' is also valid here if you don't need a callback. - public static void ReregisterSaveLoadGameData(string modGuid, T saveData, Action saveEventCallback, Action loadEventCallback) + /// T saveData object you passed as a parameter, or a default instance of object T if you didn't pass anything + public static T ReregisterSaveLoadGameData(string modGuid, Action onSave, Action onLoad, T saveData = default(T)) { UnRegisterSaveLoadGameData(modGuid); - RegisterSaveLoadGameData(modGuid, saveData, saveEventCallback, loadEventCallback); + return RegisterSaveLoadGameData(modGuid, onSave, onLoad, saveData); } } From 219d8ff460ae4f3f03d61ce879de4071d7e7dd2f Mon Sep 17 00:00:00 2001 From: Falki-git <72734856+Falki-git@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:57:41 +0100 Subject: [PATCH 3/3] Typo --- SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs index 49fcbc4f..13c032d3 100644 --- a/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs +++ b/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs @@ -20,7 +20,7 @@ internal class SaveLoadPatches [HarmonyPatch(new Type[] { typeof(string), typeof(LoadGameData) })] private static void InjectPluginSaveGameData(string filename, LoadGameData data, SerializeGameDataFlowAction __instance) { - // Skip plugin data injection if there are not mods that have registered for save/load actions + // Skip plugin data injection if there are no mods that have registered for save/load actions if (ModSaves.InternalPluginSaveData.Count == 0) return;