From afa4c21b29ed569e3396ad6d87667f0302e7a56d Mon Sep 17 00:00:00 2001 From: Leonardo Ascione <112330494+rockfactory@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:36:04 +0100 Subject: [PATCH] Fix mods save data loading when reverting to VAB FlowActions used to serialize/deserialize in memory are different from the ones used to load and save the game from JSON files --- .../SaveGameManager/SaveGamePatches.cs | 166 ++++++++++++++---- 1 file changed, 127 insertions(+), 39 deletions(-) diff --git a/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs b/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs index a77deae..1d3b206 100644 --- a/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs +++ b/src/SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs @@ -1,6 +1,8 @@ using HarmonyLib; +using KSP.Game; using KSP.Game.Load; using KSP.IO; +using Newtonsoft.Json; using SpaceWarp.API.SaveGameManager; using SpaceWarp.Backend.SaveGameManager; using SpaceWarp.InternalUtilities; @@ -11,6 +13,24 @@ namespace SpaceWarp.Patching.SaveGameManager; internal class SaveLoadPatches { #region Saving + + /// + /// Common method used before serialization to save plugin data, if any. + /// + private static void SavePluginData(LoadGameData data) + { + // 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); + } + } [HarmonyPatch(typeof(SerializeGameDataFlowAction), MethodType.Constructor, [typeof(string), typeof(LoadGameData)])] [HarmonyPostfix] @@ -27,26 +47,122 @@ SerializeGameDataFlowAction __instance 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) + SavePluginData(data); + } + + /// + /// Handles save game serialization in memory, like when launching from VAB. Current + /// game is serialized to a buffer and kept in memory. + /// + [HarmonyPatch(typeof(SerializeGameToMemoryFlowAction), MethodType.Constructor, [typeof(LoadOrSaveCampaignTicket)])] + [HarmonyPostfix] + private static void InjectToMemoryPluginSaveGameData( + LoadOrSaveCampaignTicket loadOrSaveCampaignTicket, + // ReSharper disable once InconsistentNaming + SerializeGameToMemoryFlowAction __instance + ) + { + // Skip plugin data injection if there are no mods that have registered for save/load actions + if (ModSaves.InternalPluginSaveData.Count == 0) { - plugin.SaveEventCallback(plugin.SaveData); + return; } + + SavePluginData(loadOrSaveCampaignTicket.LoadGameData); } #endregion #region Loading + /// + /// Common method used after deserialization to load plugin data, if any. + /// + private static void LoadPluginSaveData(SpaceWarpSerializedSavedGame serializedSavedGame) + { + // Perform plugin load data if plugin data is found in the save file + if (serializedSavedGame.serializedPluginSaveData.Count <= 0) return; + + // 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) + { + SpaceWarpPlugin.Instance.SWLogger.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 + ); + } + } + + /// + /// DeserializeBufferFlowAction is used when reverting to VAB / Launch from flight + /// + [HarmonyPatch(typeof(DeserializeBufferFlowAction), "DoAction")] + [HarmonyPrefix] + private static bool DeserializeBufferLoadedPluginData( + Action resolve, + Action reject, + // ReSharper disable once InconsistentNaming + DeserializeBufferFlowAction __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 + { + if (DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings == null) + { + DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings = IOProvider.CloneSerializerSettings(IOProvider.GetDefaultSerializerSettings()); + DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings.NullValueHandling = NullValueHandling.Ignore; + } + + // Deserialize save buffer to our own class that extends game's SerializedSavedGame + var serializedSavedGame = IOProvider.FromBuffer(__instance._savedGameBuffer, DeserializeBufferFlowAction._ignoreNullValueSerialzationSettings); + __instance._data.SavedGame = serializedSavedGame; + __instance._data.DataLength = (long) __instance._savedGameBuffer.Length; + + // Perform plugin load data if plugin data is found in the save file + LoadPluginSaveData(serializedSavedGame); + } + catch (Exception ex) + { + UnityEngine.Debug.LogException(ex); + reject(ex.Message); + } + + resolve(); + + return false; + } + + /// + /// DeserializeContentsFlowAction is used when loading a save file + /// [HarmonyPatch(typeof(DeserializeContentsFlowAction), "DoAction")] [HarmonyPrefix] - private static bool DeserializeLoadedPluginData( + private static bool DeserializeContentsLoadedPluginData( Action resolve, Action reject, // ReSharper disable once InconsistentNaming @@ -68,35 +184,7 @@ DeserializeContentsFlowAction __instance __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) - { - SpaceWarpPlugin.Instance.SWLogger.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 - ); - } - } + LoadPluginSaveData(serializedSavedGame); } catch (Exception ex) {