-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #270 from Falki-git/plugin-data-in-save-files
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
Showing
5 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
13 changes: 13 additions & 0 deletions
13
SpaceWarp.Core/Backend/SaveGameManager/SpaceWarpSerializedSavedGame.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
SpaceWarp.Core/Patching/SaveGameManager/SaveGamePatches.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |