diff --git a/PulsarModLoader/Patches/PLGlobalStart.cs b/PulsarModLoader/Patches/PLGlobalStart.cs index 9d1d974..364a123 100644 --- a/PulsarModLoader/Patches/PLGlobalStart.cs +++ b/PulsarModLoader/Patches/PLGlobalStart.cs @@ -29,7 +29,10 @@ static void Prefix() //Modmanager GUI Init. new GameObject("ModManager", typeof(CustomGUI.GUIMain)) { hideFlags = HideFlags.HideAndDontSave }; - + + //SaveDataManager Init() + new SaveData.SaveDataManager(); + //ModLoading string modsDir = Path.Combine(Directory.GetCurrentDirectory(), "Mods"); ModManager.Instance.LoadModsDirectory(modsDir); diff --git a/PulsarModLoader/PulsarModLoader.csproj b/PulsarModLoader/PulsarModLoader.csproj index 55c4956..c846c56 100644 --- a/PulsarModLoader/PulsarModLoader.csproj +++ b/PulsarModLoader/PulsarModLoader.csproj @@ -215,6 +215,9 @@ + + + diff --git a/PulsarModLoader/SaveData/DisplayModdedSavePatch.cs b/PulsarModLoader/SaveData/DisplayModdedSavePatch.cs new file mode 100644 index 0000000..000aa90 --- /dev/null +++ b/PulsarModLoader/SaveData/DisplayModdedSavePatch.cs @@ -0,0 +1,95 @@ +using HarmonyLib; +using PulsarModLoader.Patches; +using PulsarModLoader.Utilities; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Emit; + +namespace PulsarModLoader.SaveData +{ + [HarmonyPatch(typeof(PLUILoadMenu), "Update")] + class DisplayModdedSavePatch + { + public static List MFileNames = new List(); + static string AppendModdedLine(string originalText, PLUILoadMenu instance) + { + string Cachedname = PLNetworkManager.Instance.FileNameToRelative(instance.DataToLoad.FileName); + if (Cachedname.StartsWith("Saves/")) + { + Cachedname = Cachedname.Remove(0, 6); + } + if (instance.DataToLoad != null && MFileNames.Contains(Cachedname)) + { + //Logger.Info("Appending GameInfo Line"); + originalText += "\nModded" + SaveDataManager.ReadMods; + } + return originalText; + } + static string CheckAddPMLSaveFileTag(string inFileName) + { + if(MFileNames.Contains(inFileName)) + { + return "M " + inFileName; + } + else + { + return inFileName; + } + } + static IEnumerable Transpiler(IEnumerable instructions) + { + //Handle Load Preview display + List targetsequence = new List() + { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PLUILoadMenu), "GameInfoLabel")), + new CodeInstruction(OpCodes.Ldloc_S) //(byte)14 + }; + List injectedsequence = new List() + { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(DisplayModdedSavePatch), "AppendModdedLine")) + }; + + IEnumerable patchedInstructions = HarmonyHelpers.PatchBySequence(instructions, targetsequence, injectedsequence, checkMode: HarmonyHelpers.CheckMode.NONNULL); + + //Handle Loadmenu Elements display + targetsequence = new List() + { + new CodeInstruction(OpCodes.Ldloc_S), //(byte)7 + new CodeInstruction(OpCodes.Ldfld), + new CodeInstruction(OpCodes.Ldloc_S) //(byte)8 + }; + injectedsequence = new List() + { + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(DisplayModdedSavePatch), "CheckAddPMLSaveFileTag")) + }; + return HarmonyHelpers.PatchBySequence(patchedInstructions, targetsequence, injectedsequence, checkMode: HarmonyHelpers.CheckMode.NONNULL); + } + } + + + [HarmonyPatch(typeof(PLSaveGameIO), "AddSaveGameDataBasicFromDir")] + class ListModdedSavesPatch + { + static void Postfix() + { + List MFiles = new List(); + foreach(SaveGameDataBasic saveDataBasic in PLSaveGameIO.Instance.SaveGamesBasic) + { + string Cachedname = saveDataBasic.FileName; + string moddedFileName = SaveDataManager.getPMLSaveFileName(Cachedname); + if (File.Exists(moddedFileName)) + { + Cachedname = PLNetworkManager.Instance.FileNameToRelative(Cachedname); + if (Cachedname.StartsWith("Saves/")) + { + Cachedname = Cachedname.Remove(0, 6); + } + MFiles.Add(Cachedname); + } + } + DisplayModdedSavePatch.MFileNames = MFiles; + } + } +} diff --git a/PulsarModLoader/SaveData/PMLSaveData.cs b/PulsarModLoader/SaveData/PMLSaveData.cs new file mode 100644 index 0000000..ec1aa3f --- /dev/null +++ b/PulsarModLoader/SaveData/PMLSaveData.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace PulsarModLoader.SaveData +{ + public abstract class PMLSaveData + { + public PulsarMod MyMod; + public abstract string Identifier(); + public virtual uint VersionID => 0; + public abstract MemoryStream SaveData(); + public abstract void LoadData(MemoryStream dataStream, uint VersionID); + } +} diff --git a/PulsarModLoader/SaveData/SaveDataManager.cs b/PulsarModLoader/SaveData/SaveDataManager.cs new file mode 100644 index 0000000..a25e640 --- /dev/null +++ b/PulsarModLoader/SaveData/SaveDataManager.cs @@ -0,0 +1,303 @@ +using HarmonyLib; +using PulsarModLoader.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace PulsarModLoader.SaveData +{ + class SaveDataManager + { + public SaveDataManager() + { + ModManager.Instance.OnModSuccessfullyLoaded += OnModLoaded; + ModManager.Instance.OnModUnloaded += OnModRemoved; + Instance = this; + } + public static SaveDataManager Instance; + + public static string ReadMods = ""; + static public string SaveDir = Directory.GetCurrentDirectory() + "/Saves"; + static public string LocalSaveDir = SaveDir + "/Local"; + + List SaveConfigs = new List(); + public int SaveCount = 0; + + void OnModLoaded(string modName, PulsarMod mod) + { + mod.GetType().Assembly.GetTypes().AsParallel().ForAll((type) => + { + if (typeof(PMLSaveData).IsAssignableFrom(type) && !type.IsAbstract) + { + PMLSaveData SaveData = (PMLSaveData)Activator.CreateInstance(type); + SaveData.MyMod = mod; + SaveConfigs.Add(SaveData); + SaveCount = SaveConfigs.Count; + } + }); + } + + void OnModRemoved(PulsarMod mod) + { + List saveConfigsToRemove = new List(); + SaveConfigs.AsParallel().ForAll((arg) => + { + if (arg.GetType().Assembly == mod.GetType().Assembly) + { + saveConfigsToRemove.Add(arg); + } + }); + for (byte s = 0; s < saveConfigsToRemove.Count; s++) + { + SaveConfigs.Remove(saveConfigsToRemove[s]); + SaveCount = SaveConfigs.Count; + } + } + + public static string getPMLSaveFileName(string inFileName) + { + if (inFileName.EndsWith("LastRecoveredSave.plsave")) //Fix LastRecoveredSave + { + return inFileName.Replace("LastRecoveredSave.plsave", "LastRecoveredMSave.pmlsave"); + } + return inFileName.Replace(".plsave", ".pmlsave"); + } + + public void SaveDatas(string inFileName) + { + //Stop if no save configs to save. Additionally check for older .pmlsave file under the same name and delete. + string fileName = getPMLSaveFileName(inFileName); + if (SaveCount == 0) + { + if (File.Exists(fileName)) + { + Logger.Info("Old PMLSave found with no new data to save. Deleting old data."); + File.Delete(fileName); + } + return; + } + + //Start Saving, create temp file + string tempText = fileName + "_temp"; + FileStream fileStream = File.Create(tempText); + BinaryWriter binaryWriter = new BinaryWriter(fileStream); + + //save for mods + binaryWriter.Write(SaveCount); //int32 representing total configs + foreach (PMLSaveData saveData in SaveConfigs) + { + try + { + PulsarModLoader.Utilities.Logger.Info($"Writing: {saveData.MyMod.HarmonyIdentifier()}::{saveData.Identifier()}"); + MemoryStream dataStream = saveData.SaveData(); //Collect Save data from mod + int bytecount = (int)dataStream.Length; + + //SaveDataHeader + binaryWriter.Write(saveData.MyMod.HarmonyIdentifier()); //Write Mod Identifier + binaryWriter.Write(saveData.Identifier()); //Write PMLSaveData Identifier + binaryWriter.Write(saveData.VersionID); //Write PMLSaveData VersionID + binaryWriter.Write(bytecount); //Write stream byte count + dataStream.Position = 0; //Reset position of dataStream for reading + + byte[] buffer = new byte[bytecount]; + dataStream.Read(buffer, 0, bytecount); //move data to filestream + binaryWriter.BaseStream.Write(buffer, 0, bytecount); + + dataStream.Close(); + } + catch (Exception ex) + { + Logger.Info($"Failed to save a mod data.\n{ex.Message}"); + } + } + + //Finish Saving, close and save file to actual location + binaryWriter.Close(); + fileStream.Close(); + if (File.Exists(fileName)) + { + File.Delete(fileName); + } + File.Move(tempText, fileName); + string relativeFileName = PLNetworkManager.Instance.FileNameToRelative(fileName); + Logger.Info("PMLSaveManager has saved file: " + relativeFileName); + + + //Save to Steam + /*bool localFile = false; + if (relativeFileName.StartsWith(LocalSaveDir)) + { + localFile = true; + } + if (!PLServer.Instance.IronmanModeIsActive && !localFile) + { + Logger.Info("Should be saving file to steam cloud"); + PLNetworkManager.Instance.SteamCloud_WriteFileName(relativeFileName, delegate (RemoteStorageFileWriteAsyncComplete_t pCallback, bool bIOFailure) + { + OnRemoteFileWriteAsyncComplete(pCallback, bIOFailure, relativeFileName); + }); + }*/ + } + + public void LoadDatas(string inFileName) + { + //start reading + string fileName = getPMLSaveFileName(inFileName); + if (!File.Exists(fileName)) + { + return; + } + FileStream fileStream = File.OpenRead(fileName); + BinaryReader binaryReader = new BinaryReader(fileStream); + + //read for mods + int count = binaryReader.ReadInt32(); //int32 representing total configs + string missingMods = ""; + string VersionMismatchedMods = ""; + string readMods = ""; + for (int i = 0; i < count; i++) + { + //SaveDataHeader + string harmonyIdent = binaryReader.ReadString(); //HarmonyIdentifier + string SavDatIdent = binaryReader.ReadString(); //SaveDataIdentifier + uint VersionID = binaryReader.ReadUInt32(); //VersionID + int bytecount = binaryReader.ReadInt32(); //ByteCount + PulsarModLoader.Utilities.Logger.Info($"Reading SaveData: {harmonyIdent}::{SavDatIdent} SaveDataVersion: {VersionID} bytecount: {bytecount} Pos: {binaryReader.BaseStream.Position}"); + readMods += "\n" + harmonyIdent; + + + bool foundReader = false; + foreach (PMLSaveData savedata in SaveConfigs) + { + if (savedata.MyMod.HarmonyIdentifier() == harmonyIdent && savedata.Identifier() == SavDatIdent) + { + if (VersionID != savedata.VersionID) + { + Logger.Info($"Mismatched SaveData VersionID. Read: {VersionID} SaveData: {savedata.VersionID}"); + VersionMismatchedMods += "\n" + harmonyIdent; + } + MemoryStream stream = new MemoryStream(); //initialize new memStream + + byte[] buffer = new byte[bytecount]; + binaryReader.BaseStream.Read(buffer, 0, bytecount); //move data to memStream + stream.Write(buffer, 0, bytecount); + + stream.Position = 0; //Reset position + try + { + savedata.LoadData(stream, VersionID); //Send memStream to PMLSaveData + } + catch (Exception ex) + { + Logger.Info($"Failed to load {harmonyIdent}::{SavDatIdent}\n{ex.Message}"); + } + stream.Close(); + foundReader = true; + } + } + if (!foundReader) + { + binaryReader.BaseStream.Position += bytecount; + missingMods += ("\n" + harmonyIdent); + } + } + + //Finish Reading + binaryReader.Close(); + fileStream.Close(); + Logger.Info("PMLSaveManager has read file: " + PLNetworkManager.Instance.FileNameToRelative(fileName)); + ReadMods = readMods; + + if (missingMods.Length > 0) + { + PLNetworkManager.Instance.MainMenu.AddActiveMenu(new PLErrorMessageMenu($"Warning: Found save data for following missing mods: {missingMods}")); + Logger.Info($"Warning: Found save data for following missing mods: {missingMods}"); + } + if (!string.IsNullOrEmpty(VersionMismatchedMods)) + { + PLNetworkManager.Instance.MainMenu.AddActiveMenu(new PLErrorMessageMenu($"Warning: The following mods used in this save have been updated: {VersionMismatchedMods}")); + Logger.Info($"Warning: The following mods used in this save have been updated: {VersionMismatchedMods}"); + } + } + } + [HarmonyPatch(typeof(PLSaveGameIO), "SaveToFile")] + class SavePatch + { + //Attempt at blocking saving of .plsave to steam when modded data exists. + /*static bool PatchMethod(string a) + { + if(SaveDataManager.Instance.SaveCount > 0) + { + Logger.Info("Running PatchMethod"); + return true; + } + else + { + return a.StartsWith(SaveDataManager.LocalSaveDir); + } + } + static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List targetsequence = new List() + { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PLSaveGameIO), "LocalSaveDir")), + new CodeInstruction(OpCodes.Call), + new CodeInstruction(OpCodes.Callvirt), + }; + List injectedsequence = new List() + { + new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(SavePatch), "PatchMethod")), + }; + return HarmonyHelpers.PatchBySequence(instructions, targetsequence, injectedsequence, HarmonyHelpers.PatchMode.REPLACE, HarmonyHelpers.CheckMode.NONNULL); + }*/ + static void Postfix(string inFileName) + { + SaveDataManager.Instance.SaveDatas(inFileName); + } + } + [HarmonyPatch(typeof(PLSaveGameIO), "LoadFromFile")] + class LoadPatch + { + static void Postfix(string inFileName) + { + SaveDataManager.Instance.LoadDatas(inFileName); + } + } + + + [HarmonyPatch(typeof(PLSaveGameIO), "DeleteSaveGame")] + class DeletePatch + { + static void Postfix(PLSaveGameIO __instance) + { + string fileName = SaveDataManager.getPMLSaveFileName(__instance.LatestSaveGameFileName); + if (fileName != "") + { + try + { + Logger.Info("DeleteSaveGame " + fileName); + File.Delete(__instance.LatestSaveGameFileName); + } + catch (Exception ex) + { + Logger.Info("DeleteSaveGame EXCEPTION: " + ex.Message + ": Could not delete save file!"); + } + } + } + } + + [HarmonyPatch(typeof(PLUILoadMenu), "OnClickDeleteSaveFile")] + class LoadMenuDeletePatch + { + static void Postfix(string inFileName) + { + if (inFileName.EndsWith(".plsave")) + { + string PMLname = SaveDataManager.getPMLSaveFileName(inFileName); + typeof(PLUILoadMenu).GetMethod("OnClickDeleteSaveFile", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(PLUILoadMenu.Instance, new object[] { PMLname }); + } + } + } +}