diff --git a/ModuleManager.csproj b/ModuleManager.csproj
index 641257a1..14d13b60 100644
--- a/ModuleManager.csproj
+++ b/ModuleManager.csproj
@@ -32,6 +32,7 @@
+
diff --git a/SaveGameFixer.cs b/SaveGameFixer.cs
new file mode 100644
index 00000000..e41df113
--- /dev/null
+++ b/SaveGameFixer.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace ModuleManager
+{
+ [KSPAddonFixed(KSPAddon.Startup.MainMenu, true, typeof(SaveGameFixer))]
+ internal class SaveGameFixer : MonoBehaviour
+ {
+
+ #region type election and other bootstrap stuff.
+
+ // This is stolen unchanged from KSPAPIExtensions
+ private static bool RunTypeElection(Type targetCls, String assemName)
+ {
+ if (targetCls.Assembly.GetName().Name != assemName)
+ throw new InvalidProgramException("Assembly: " + targetCls.Assembly.GetName().Name + " at location: " + targetCls.Assembly.Location + " is not in the expected assembly. Code has been copied and this will cause problems.");
+
+ // If we are loaded from the first loaded assembly that has this class, then we are responsible to destroy
+ var candidates = (from ass in AssemblyLoader.loadedAssemblies
+ where ass.assembly.GetType(targetCls.FullName, false) != null
+ && ass.assembly.GetName().Name == assemName
+ orderby ass.assembly.GetName().Version descending, ass.path ascending
+ select ass).ToArray();
+ var winner = candidates.First();
+
+ if (targetCls.Assembly != winner.assembly)
+ return false;
+
+ if (candidates.Length > 1)
+ {
+ string losers = string.Join("\n", (from t in candidates
+ where t != winner
+ select string.Format("Version: {0} Location: {1}", t.assembly.GetName().Version, t.path)).ToArray());
+
+ Debug.Log("[" + targetCls.Name + "] version " + winner.assembly.GetName().Version + " at " + winner.path + " won the election against\n" + losers);
+ }
+ else
+ Debug.Log("[" + targetCls.Name + "] Elected unopposed version= " + winner.assembly.GetName().Version + " at " + winner.path);
+
+ return true;
+ }
+
+ internal void Awake()
+ {
+ try
+ {
+ if (!RunTypeElection(typeof(SaveGameFixer), "ModuleManager"))
+ return;
+ // So at this point we know we have won the election, and will be using the class versions as in this assembly.
+
+ UpdateSaves();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogException(ex);
+ }
+ finally
+ {
+ // Destroy ourself because there's no reason to still hang around
+ UnityEngine.Object.Destroy(gameObject);
+ enabled = false;
+ }
+ }
+ #endregion
+
+ #region Finding the part
+ private string savesRoot;
+
+ private void UpdateSaves()
+ {
+ savesRoot = Path.Combine(Path.GetFullPath(KSPUtil.ApplicationRootPath), "saves" + Path.DirectorySeparatorChar);
+
+ foreach (string saveDir in Directory.GetDirectories(savesRoot))
+ UpdateSaveDir(saveDir);
+ }
+
+
+ private void UpdateSaveDir(string saveDir)
+ {
+ try
+ {
+ PushLogContext("Save Game: " + saveDir.Substring(savesRoot.Length, saveDir.Length-savesRoot.Length));
+
+ char ds = Path.DirectorySeparatorChar;
+
+ // .craft files
+ UpdateCraftDir(saveDir + ds + "Ships" + ds + "VAB");
+ UpdateCraftDir(saveDir + ds + "Ships" + ds + "SPH");
+ UpdateCraftDir(saveDir + ds + "Subassemblies");
+
+ foreach (string sfsFile in Directory.GetFiles(saveDir))
+ if (sfsFile.EndsWith(".sfs"))
+ UpdateSFS(sfsFile);
+ }
+ finally
+ {
+ PopLogContext();
+ }
+ }
+
+ private void UpdateCraftDir(string dir)
+ {
+ string[] files;
+ try
+ {
+ files = Directory.GetFiles(dir);
+ }
+ catch (Exception)
+ {
+ return;
+ }
+ foreach (string vabCraft in Directory.GetFiles(dir))
+ if (vabCraft.EndsWith(".craft"))
+ UpdateCraft(vabCraft);
+ }
+
+ private void UpdateCraft(string vabCraft)
+ {
+ try
+ {
+ PushLogContext("Craft file: " + vabCraft.Substring(savesRoot.Length, vabCraft.Length-savesRoot.Length));
+ ConfigNode craft = ConfigNode.Load(vabCraft);
+
+ bool modified = false;
+ foreach (ConfigNode part in craft.GetNodes("PART"))
+ modified |= UpdatePart(part);
+
+ if (modified)
+ BackupAndReplace(vabCraft, craft);
+ }
+ finally
+ {
+ PopLogContext();
+ }
+ }
+
+ private void UpdateSFS(string sfsFile)
+ {
+ ConfigNode sfs = ConfigNode.Load(sfsFile);
+
+ try
+ {
+ PushLogContext("Save file: " + sfsFile.Substring(savesRoot.Length, sfsFile.Length-savesRoot.Length));
+
+ bool modified = false;
+ foreach (ConfigNode game in sfs.GetNodes("GAME"))
+ foreach (ConfigNode flightState in game.GetNodes("FLIGHTSTATE"))
+ foreach (ConfigNode vessel in flightState.GetNodes("VESSEL"))
+ modified |= UpdateVessel(vessel);
+
+ if (modified)
+ BackupAndReplace(sfsFile, sfs);
+ }
+ finally
+ {
+ PopLogContext();
+ }
+
+ }
+
+ private bool UpdateVessel(ConfigNode vessel)
+ {
+ try
+ {
+ PushLogContext("Vessel: " + vessel.GetValue("name"));
+
+ bool modified = false;
+ foreach (ConfigNode part in vessel.GetNodes("PART"))
+ modified |= UpdatePart(part);
+ return modified;
+ }
+ finally
+ {
+ PopLogContext();
+ }
+ }
+ #endregion
+
+ private bool UpdatePart(ConfigNode part)
+ {
+ // The modules saved with the part
+ ConfigNode[] savedModules = part.GetNodes("MODULE");
+ int savedRemain = savedModules.Length;
+
+ //Debug.LogWarning("Saved modules: " + string.Join(",", (from s in savedModules select s.GetValue("name")).ToArray()));
+
+ // The modules saved as backups
+ ConfigNode[] backupModules = new ConfigNode[0];
+ for(int i = 0; i < savedModules.Length; ++i)
+ if (savedModules[i].GetValue("name") == typeof(ModuleConfigBackup).Name)
+ {
+ backupModules = savedModules[i].GetNodes("MODULE");
+ savedModules[i] = null;
+ savedRemain--;
+ break;
+ }
+ //Debug.LogWarning("Backup modules: " + string.Join(",", (from s in backupModules select s.GetValue("name")).ToArray()));
+ int backupRemain = backupModules.Length;
+
+ // The modules in the part prefab
+ string partName = part.GetValue("name");
+ if (partName == null)
+ {
+ partName = part.GetValue("part");
+ partName = partName.Substring(0, partName.LastIndexOf('_'));
+ }
+
+ try
+ {
+ PushLogContext("Part: " + partName);
+
+ AvailablePart available = PartLoader.getPartInfoByName(partName);
+
+ if (available == null)
+ {
+ WriteLogMessage("Backup created - part \"" + partName + "\" has been deleted and ship will be destroyed.");
+ return false;
+ }
+
+ PartModuleList prefabModules = available.partPrefab.Modules;
+
+ if (prefabModules == null)
+ return false;
+
+ // Do we need to do anything?
+ if (prefabModules.Count == savedModules.Length && backupModules.Length == 0)
+ {
+ for (int i = 0; i < savedModules.Length; ++i)
+ if (savedModules[i] != null && savedModules[i].GetValue("name") != prefabModules[i].moduleName)
+ goto needUpdate;
+ return false;
+ needUpdate: ;
+ }
+
+ // Yes we do!
+#if false
+ string prefabNames = "Prefab modules: ";
+ for (int i = 0; i < prefabModules.Count; ++i)
+ prefabNames += (prefabModules[i] as PartModule).moduleName + ",";
+ prefabNames = prefabNames.Substring(0, prefabNames.Length-1);
+
+ Debug.Log("[SaveGameFixer] Fixing Part: " + partName + " in file: " + source + "\n" + prefabNames
+ + "\nSaved modules: " + string.Join(",", (from s in savedModules select (s==null?"***":s.GetValue("name"))).ToArray())
+ + "\nBackup modules: " + string.Join(",", (from s in backupModules select (s==null?"***":s.GetValue("name"))).ToArray())
+ //+ "\nConfig: \n" + part
+ );
+#endif
+ bool hasChanged = false;
+
+ // Discard any backups that are already in saved modules
+ for (int i = 0; i < backupModules.Length; ++i)
+ for (int j = 0; j < savedModules.Length; ++j)
+ if (savedModules[j] != null && backupModules[i].GetValue("name") == savedModules[j].GetValue("name"))
+ {
+ backupModules[i] = null;
+ backupRemain--;
+ hasChanged = true;
+ }
+
+
+ part.RemoveNodes("MODULE");
+
+ ConfigNode moduleBackupConfig = null;
+
+ for (int i = 0; i < prefabModules.Count; ++i)
+ {
+ if (prefabModules[i] is ModuleConfigBackup)
+ {
+ moduleBackupConfig = new ConfigNode("MODULE");
+ moduleBackupConfig.AddValue("name", typeof(ModuleConfigBackup).Name);
+ part.AddNode(moduleBackupConfig);
+ continue;
+ }
+ for (int j = 0; j < savedModules.Length; ++j)
+ if (savedModules[j] != null && savedModules[j].GetValue("name") == prefabModules[i].moduleName)
+ {
+ // The module is saved normally
+ if (i != j)
+ {
+ WriteLogMessage("Module \"" + savedModules[j].GetValue("name") + "\" has had order changed. " + j + "=>" + i);
+ hasChanged = true;
+ }
+
+ part.AddNode(savedModules[j]);
+ savedModules[j] = null;
+ savedRemain--;
+ goto foundModule;
+ }
+ for (int j = 0; j < backupModules.Length; ++j)
+ if (backupModules[j] != null && backupModules[j].GetValue("name") == prefabModules[i].moduleName)
+ {
+ // The module will be restored from backup
+ WriteLogMessage("Module \"" + backupModules[j].GetValue("name") + "\" has been restored from backup. ");
+ hasChanged = true;
+
+ backupModules[j].AddValue("MM_RESTORED", "true");
+ part.AddNode(backupModules[j]);
+ backupModules[j] = null;
+ backupRemain--;
+ goto foundModule;
+ }
+ // Can't find it anywhere, reinitialize
+ WriteLogMessage("Module \"" + prefabModules[i].moduleName + "\" is not present in the save and will be reinitialized. ");
+ hasChanged = true;
+
+ ConfigNode newNode = new ConfigNode("MODULE");
+ newNode.AddValue("name", prefabModules[i].moduleName);
+ newNode.AddValue("MM_REINITIALIZE", "true");
+ part.AddNode(newNode);
+ foundModule: ;
+ }
+
+ if (savedRemain > 0 || backupRemain > 0)
+ {
+
+ // Discard saves for modules that are explicitly marked as dynamic or have a module available to be used.
+ // Modules that are explicitly maked as not dynamic (MM_DYNAMIC = false) will be saved in the backup regardless
+ // of if their PartModule class is available.
+ for (int i = 0; i < savedModules.Length; ++i)
+ if (savedModules[i] != null && savedModules[i].GetValue("MM_DYNAMIC") != "false"
+ && (savedModules[i].GetValue("MM_DYNAMIC") == "true" || AssemblyLoader.GetClassByName(typeof(PartModule), savedModules[i].GetValue("name")) != null))
+ {
+ savedModules[i] = null;
+ --savedRemain;
+ }
+
+ if (savedRemain > 0)
+ {
+ if (moduleBackupConfig == null)
+ {
+ available.partPrefab.AddModule(typeof(ModuleConfigBackup).Name);
+ moduleBackupConfig = new ConfigNode("MODULE");
+ moduleBackupConfig.AddValue("name", typeof(ModuleConfigBackup).Name);
+ part.AddNode(moduleBackupConfig);
+ }
+ // copy the old backups
+ for (int i = 0; i < backupModules.Length; ++i)
+ if (backupModules[i] != null)
+ moduleBackupConfig.AddNode(backupModules[i]);
+ // backup anything in saved that's left over
+ for (int i = 0; i < savedModules.Length; ++i)
+ if (savedModules[i] != null)
+ {
+ savedModules[i].RemoveValues("MM_RESTORED");
+ moduleBackupConfig.AddNode(savedModules[i]);
+
+ WriteLogMessage("Module \"" + savedModules[i].GetValue("name") + "\" is present in the part but is no longer available. Saved config to backup, will be restored if you reinstall the mod.");
+ hasChanged = true;
+ }
+ }
+ }
+
+ if (!hasChanged)
+ return false;
+
+ // Stick the resources back at the end just to be consistent
+ ConfigNode[] resources = part.GetNodes("RESOURCE");
+ part.RemoveNodes("RESOURCE");
+ foreach (ConfigNode r in resources)
+ part.AddNode(r);
+
+ //Debug.Log("[SaveGameFixer] Result:\n" + part);
+ }
+ finally
+ {
+ PopLogContext();
+ }
+ return true;
+ }
+
+ #region Backups
+
+ private List logContext = new List();
+ private int logCtxCur = 0;
+ private string backupDir = null;
+ private string logFile = null;
+
+ private void PushLogContext(string p)
+ {
+ logContext.Add(p);
+ }
+
+ private void PopLogContext()
+ {
+ logContext.RemoveAt(logContext.Count - 1);
+ if (logCtxCur > logContext.Count)
+ logCtxCur = logContext.Count;
+ }
+
+ private void WriteLogMessage(string logMessage)
+ {
+ if (backupDir == null)
+ {
+ backupDir = Path.Combine(KSPUtil.ApplicationRootPath, string.Format("saves_backup{1}{0:yyyyMMdd-HHmmss}", DateTime.Now, Path.DirectorySeparatorChar));
+ Directory.CreateDirectory(backupDir);
+ logFile = Path.Combine(backupDir, "backup.log");
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ // Write any pending log headers
+ string indent;
+ for (; logCtxCur < logContext.Count; logCtxCur++)
+ {
+ indent = new String(' ', 4 * logCtxCur);
+ sb.Append(indent).AppendLine(logContext[logCtxCur]);
+ Debug.Log("[SaveGameFixer]" + indent + logContext[logCtxCur]);
+ }
+ indent = new String(' ', 4 * logCtxCur);
+ sb.Append(indent).AppendLine(logMessage);
+ Debug.Log("[SaveGameFixer]" + indent + logMessage);
+
+ File.AppendAllText(logFile, sb.ToString());
+ }
+
+ private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
+ {
+ // Get the subdirectories for the specified directory.
+ DirectoryInfo dir = new DirectoryInfo(sourceDirName);
+ DirectoryInfo[] dirs = dir.GetDirectories();
+
+ if (!dir.Exists)
+ {
+ throw new DirectoryNotFoundException(
+ "Source directory does not exist or could not be found: "
+ + sourceDirName);
+ }
+
+ // If the destination directory doesn't exist, create it.
+ if (!Directory.Exists(destDirName))
+ {
+ Directory.CreateDirectory(destDirName);
+ }
+
+ // Get the files in the directory and copy them to the new location.
+ FileInfo[] files = dir.GetFiles();
+ foreach (FileInfo file in files)
+ {
+ string temppath = Path.Combine(destDirName, file.Name);
+ file.CopyTo(temppath, false);
+ }
+
+ // If copying subdirectories, copy them and their contents to new location.
+ if (copySubDirs)
+ {
+ foreach (DirectoryInfo subdir in dirs)
+ {
+ string temppath = Path.Combine(destDirName, subdir.Name);
+ DirectoryCopy(subdir.FullName, temppath, copySubDirs);
+ }
+ }
+ }
+
+ private void BackupAndReplace(string file, ConfigNode config)
+ {
+ string relPath = file.Substring(savesRoot.Length, file.Length - savesRoot.Length);
+
+ string backupTo = Path.Combine(backupDir, relPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(backupTo));
+
+ File.Copy(file, backupTo);
+
+ config.Save(file);
+ }
+
+
+ #endregion
+ }
+
+ internal class ModuleConfigBackup : PartModule
+ {
+
+ public ConfigNode removedConfigs;
+
+ public override void OnLoad(ConfigNode node)
+ {
+ if (removedConfigs == null)
+ removedConfigs = new ConfigNode();
+
+ foreach (ConfigNode subNode in node.GetNodes("MODULE"))
+ removedConfigs.AddNode(subNode);
+ }
+
+ public override void OnSave(ConfigNode node)
+ {
+ if (removedConfigs == null)
+ return;
+
+ foreach (ConfigNode subNode in removedConfigs.GetNodes("MODULE"))
+ node.AddNode(subNode);
+ }
+ }
+
+}