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)
{