using System; using System.IO; using System.IO.Compression; using System.Text; using System.Threading; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma { partial class SaveUtil { public static string SaveFolder = "Data" + Path.DirectorySeparatorChar + "Saves"; public static string MultiplayerSaveFolder = "Data" + Path.DirectorySeparatorChar + "Saves" + Path.DirectorySeparatorChar + "Multiplayer"; public delegate void ProgressDelegate(string sMessage); public static string TempPath { #if SERVER get { return Path.Combine(SaveFolder, "temp_server"); } #else get { return Path.Combine(SaveFolder, "temp"); } #endif } public enum SaveType { Singleplayer, Multiplayer } public static void SaveGame(string filePath) { DebugConsole.Log("Saving the game to: " + filePath); Directory.CreateDirectory(TempPath); try { ClearFolder(TempPath, new string[] { GameMain.GameSession.Submarine.FilePath }); } catch (Exception e) { DebugConsole.ThrowError("Failed to clear folder", e); } try { if (Submarine.MainSub != null) { string subPath = Path.Combine(TempPath, Submarine.MainSub.Name + ".sub"); if (Submarine.Loaded.Contains(Submarine.MainSub)) { Submarine.MainSub.FilePath = subPath; Submarine.MainSub.SaveAs(Submarine.MainSub.FilePath); } else if (Submarine.MainSub.FilePath != subPath) { if (File.Exists(subPath)) { File.Delete(subPath); } File.Copy(Submarine.MainSub.FilePath, subPath); Submarine.MainSub.FilePath = subPath; } } } catch (Exception e) { DebugConsole.ThrowError("Error saving submarine", e); } try { GameMain.GameSession.Save(Path.Combine(TempPath, "gamesession.xml")); } catch (Exception e) { DebugConsole.ThrowError("Error saving gamesession", e); } try { CompressDirectory(TempPath, filePath, null); } catch (Exception e) { DebugConsole.ThrowError("Error compressing save file", e); } } public static void LoadGame(string filePath) { DebugConsole.Log("Loading save file: " + filePath); DecompressToDirectory(filePath, TempPath, null); XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); string subPath = Path.Combine(TempPath, doc.Root.GetAttributeString("submarine", "")) + ".sub"; Submarine selectedSub = new Submarine(subPath, ""); GameMain.GameSession = new GameSession(selectedSub, filePath, doc); } public static void LoadGame(string filePath, GameSession gameSession) { DebugConsole.Log("Loading save file for an existing game session (" + filePath + ")"); DecompressToDirectory(filePath, TempPath, null); XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); gameSession.Load(doc.Root); } public static XDocument LoadGameSessionDoc(string filePath) { DebugConsole.Log("Loading game session doc: " + filePath); try { DecompressToDirectory(filePath, TempPath, null); } catch (Exception e) { DebugConsole.ThrowError("Error decompressing " + filePath, e); return null; } return XMLExtensions.TryLoadXml(Path.Combine(TempPath, "gamesession.xml")); } public static void DeleteSave(string filePath) { try { File.Delete(filePath); } catch (Exception e) { DebugConsole.ThrowError("ERROR: deleting save file \"" + filePath + "\" failed.", e); } //deleting a multiplayer save file -> also delete character data if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder))) { string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath); if (File.Exists(characterDataSavePath)) { try { File.Delete(characterDataSavePath); } catch (Exception e) { DebugConsole.ThrowError("ERROR: deleting character data file \"" + characterDataSavePath + "\" failed.", e); } } } } public static string GetSavePath(SaveType saveType, string saveName) { string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; return Path.Combine(folder, saveName); } public static string[] GetSaveFiles(SaveType saveType) { string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; if (!Directory.Exists(folder)) { DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder..."); try { Directory.CreateDirectory(folder); } catch (Exception e) { DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e); } } string[] files = Directory.GetFiles(folder, "*.save"); /*for (int i = 0; i < files.Length; i++) { files[i] = Path.GetFileNameWithoutExtension(files[i]); }*/ return files; } public static string CreateSavePath(SaveType saveType, string fileName = "Save_Default") { string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder; if (fileName == "Save_Default") { fileName = TextManager.Get("SaveFile.DefaultName", true); if (fileName.Length == 0) fileName = "Save"; } if (!Directory.Exists(folder)) { DebugConsole.ThrowError("Save folder \"" + folder + "\" not found. Created new folder"); Directory.CreateDirectory(folder); } string extension = ".save"; string pathWithoutExtension = Path.Combine(folder, fileName); if (!File.Exists(pathWithoutExtension + extension)) { return pathWithoutExtension + extension; } int i = 0; while (File.Exists(pathWithoutExtension + " " + i + extension)) { i++; } return pathWithoutExtension + " " + i + extension; } public static void CompressStringToFile(string fileName, string value) { // A. // Write string to temporary file. string temp = Path.GetTempFileName(); File.WriteAllText(temp, value); // B. // Read file into byte array buffer. byte[] b; using (FileStream f = new FileStream(temp, FileMode.Open)) { b = new byte[f.Length]; f.Read(b, 0, (int)f.Length); } // C. // Use GZipStream to write compressed bytes to target file. using (FileStream f2 = new FileStream(fileName, FileMode.Create)) using (GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false)) { gz.Write(b, 0, b.Length); } } public static void CompressFile(string sDir, string sRelativePath, GZipStream zipStream) { //Compress file name char[] chars = sRelativePath.ToCharArray(); zipStream.Write(BitConverter.GetBytes(chars.Length), 0, sizeof(int)); foreach (char c in chars) zipStream.Write(BitConverter.GetBytes(c), 0, sizeof(char)); //Compress file content byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath)); zipStream.Write(BitConverter.GetBytes(bytes.Length), 0, sizeof(int)); zipStream.Write(bytes, 0, bytes.Length); } public static void CompressDirectory(string sInDir, string sOutFile, ProgressDelegate progress) { string[] sFiles = Directory.GetFiles(sInDir, "*.*", SearchOption.AllDirectories); int iDirLen = sInDir[sInDir.Length - 1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; using (FileStream outFile = new FileStream(sOutFile, FileMode.Create, FileAccess.Write, FileShare.None)) using (GZipStream str = new GZipStream(outFile, CompressionMode.Compress)) foreach (string sFilePath in sFiles) { string sRelativePath = sFilePath.Substring(iDirLen); if (progress != null) progress(sRelativePath); CompressFile(sInDir, sRelativePath, str); } } public static Stream DecompressFiletoStream(string fileName) { if (!File.Exists(fileName)) { DebugConsole.ThrowError("File \"" + fileName + " doesn't exist!"); return null; } using (FileStream originalFileStream = new FileStream(fileName, FileMode.Open)) { MemoryStream decompressedFileStream = new MemoryStream(); using (GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress)) { decompressionStream.CopyTo(decompressedFileStream); return decompressedFileStream; } } } public static bool DecompressFile(string sDir, GZipStream zipStream, ProgressDelegate progress) { //Decompress file name byte[] bytes = new byte[sizeof(int)]; int Readed = zipStream.Read(bytes, 0, sizeof(int)); if (Readed < sizeof(int)) return false; int iNameLen = BitConverter.ToInt32(bytes, 0); if (iNameLen > 255) { throw new Exception("Failed to decompress \""+sDir+"\" (file name length > 255). The file may be corrupted."); } bytes = new byte[sizeof(char)]; StringBuilder sb = new StringBuilder(); for (int i = 0; i < iNameLen; i++) { zipStream.Read(bytes, 0, sizeof(char)); char c = BitConverter.ToChar(bytes, 0); sb.Append(c); } string sFileName = sb.ToString(); if (progress != null) progress(sFileName); //Decompress file content bytes = new byte[sizeof(int)]; zipStream.Read(bytes, 0, sizeof(int)); int iFileLen = BitConverter.ToInt32(bytes, 0); bytes = new byte[iFileLen]; zipStream.Read(bytes, 0, bytes.Length); string sFilePath = Path.Combine(sDir, sFileName); string sFinalDir = Path.GetDirectoryName(sFilePath); if (!Directory.Exists(sFinalDir)) Directory.CreateDirectory(sFinalDir); using (FileStream outFile = new FileStream(sFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) outFile.Write(bytes, 0, iFileLen); return true; } public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); int maxRetries = 4; for (int i = 0; i <= maxRetries; i++) { try { using (FileStream inFile = new FileStream(sCompressedFile, FileMode.Open, FileAccess.Read, FileShare.None)) using (GZipStream zipStream = new GZipStream(inFile, CompressionMode.Decompress, true)) while (DecompressFile(sDir, zipStream, progress)) ; } catch (IOException e) { if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; } DebugConsole.NewMessage("Failed decompress file \"" + sCompressedFile + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); Thread.Sleep(250); } } } public static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs) { // Get the subdirectories for the specified directory. DirectoryInfo dir = new DirectoryInfo(sourceDirName); if (!dir.Exists) { throw new DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } DirectoryInfo[] dirs = dir.GetDirectories(); // 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); CopyFolder(subdir.FullName, temppath, copySubDirs); } } } public static void ClearFolder(string FolderName, string[] ignoredFileNames = null) { DirectoryInfo dir = new DirectoryInfo(FolderName); foreach (FileInfo fi in dir.GetFiles()) { if (ignoredFileNames != null) { bool ignore = false; foreach (string ignoredFile in ignoredFileNames) { if (Path.GetFileName(fi.FullName).Equals(Path.GetFileName(ignoredFile))) { ignore = true; break; } } if (ignore) continue; } fi.IsReadOnly = false; fi.Delete(); } foreach (DirectoryInfo di in dir.GetDirectories()) { ClearFolder(di.FullName, ignoredFileNames); di.Delete(); } } } }