3 using System.Collections.Generic;
4 using System.IO.Compression;
7 using System.Threading;
9 using System.Text.RegularExpressions;
11 using Microsoft.Xna.Framework;
12 using System.Collections.Immutable;
13 using System.Diagnostics.CodeAnalysis;
19 public const string GameSessionFileName =
"gamesession.xml";
21 private static readonly
string LegacySaveFolder = Path.Combine(
"Data",
"Saves");
22 private static readonly
string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder,
"Multiplayer");
26 public static readonly
string DefaultSaveFolder = Path.Combine(
27 Environment.GetFolderPath(Environment.SpecialFolder.Personal),
29 "Application Support",
30 "Daedalic Entertainment GmbH",
35 public static readonly
string DefaultSaveFolder = Path.Combine(
36 Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
37 "Daedalic Entertainment GmbH",
41 public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder,
"Multiplayer");
43 public static readonly
string SubmarineDownloadFolder = Path.Combine(
"Submarines",
"Downloaded");
44 public static readonly
string CampaignDownloadFolder = Path.Combine(
"Data",
"Saves",
"Multiplayer_Downloaded");
46 public static string TempPath
49 get {
return Path.Combine(GetSaveFolder(SaveType.Singleplayer),
"temp_server"); }
51 get {
return Path.Combine(GetSaveFolder(SaveType.Singleplayer),
"temp"); }
61 public static void SaveGame(
string filePath)
63 DebugConsole.Log(
"Saving the game to: " + filePath);
64 Directory.CreateDirectory(TempPath);
67 ClearFolder(TempPath,
new string[] { GameMain.GameSession.SubmarineInfo.FilePath });
71 DebugConsole.ThrowError(
"Failed to clear folder", e);
77 GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName));
81 DebugConsole.ThrowError(
"Error saving gamesession", e);
87 string? mainSubPath =
null;
88 if (GameMain.GameSession.SubmarineInfo !=
null)
90 mainSubPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name +
".sub");
91 GameMain.GameSession.SubmarineInfo.SaveAs(mainSubPath);
92 for (
int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
94 if (GameMain.GameSession.OwnedSubmarines[i].Name == GameMain.GameSession.SubmarineInfo.Name)
96 GameMain.GameSession.OwnedSubmarines[i] = GameMain.GameSession.SubmarineInfo;
101 if (GameMain.GameSession.OwnedSubmarines !=
null)
103 for (
int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
105 SubmarineInfo storedInfo = GameMain.GameSession.OwnedSubmarines[i];
106 string subPath = Path.Combine(TempPath, storedInfo.Name +
".sub");
107 if (mainSubPath == subPath) {
continue; }
108 storedInfo.SaveAs(subPath);
114 DebugConsole.ThrowError(
"Error saving submarine", e);
120 CompressDirectory(TempPath, filePath);
124 DebugConsole.ThrowError(
"Error compressing save file", e);
128 public static void LoadGame(
string filePath)
133 GameMain.GameSession =
null;
134 DebugConsole.Log(
"Loading save file: " + filePath);
135 DecompressToDirectory(filePath, TempPath);
137 XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName));
138 if (doc ==
null) {
return; }
140 if (!IsSaveFileCompatible(doc))
142 throw new Exception($
"The save file \"{filePath}\" is not compatible with this version of Barotrauma.");
145 var ownedSubmarines = LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub);
146 GameMain.GameSession =
new GameSession(selectedSub, ownedSubmarines, doc, filePath);
149 public static List<SubmarineInfo> LoadOwnedSubmarines(XDocument saveDoc, out SubmarineInfo selectedSub)
151 string subPath = Path.Combine(TempPath, saveDoc.Root.GetAttributeString(
"submarine",
"")) +
".sub";
152 selectedSub =
new SubmarineInfo(subPath);
154 List<SubmarineInfo> ownedSubmarines =
new List<SubmarineInfo>();
156 var ownedSubsElement = saveDoc.Root?.Element(
"ownedsubmarines");
157 if (ownedSubsElement ==
null) {
return ownedSubmarines; }
159 foreach (var subElement
in ownedSubsElement.Elements())
161 string subName = subElement.GetAttributeString(
"name",
"");
162 string ownedSubPath = Path.Combine(TempPath, subName +
".sub");
163 if (!File.Exists(ownedSubPath))
165 DebugConsole.ThrowError($
"Could not find the submarine \"{subName}\" ({ownedSubPath})! The save file may be corrupted. Removing the submarine from owned submarines...");
169 ownedSubmarines.Add(
new SubmarineInfo(ownedSubPath));
172 return ownedSubmarines;
175 public static bool IsSaveFileCompatible(XDocument? saveDoc)
176 => IsSaveFileCompatible(saveDoc?.Root);
178 public static bool IsSaveFileCompatible(XElement? saveDocRoot)
180 if (saveDocRoot?.Attribute(
"version") ==
null) {
return false; }
184 public static void DeleteSave(
string filePath)
188 File.Delete(filePath);
192 DebugConsole.ThrowError(
"ERROR: deleting save file \"" + filePath +
"\" failed.", e);
196 var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath) ??
"");
198 if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) ||
199 fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer)))
201 string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath);
202 if (File.Exists(characterDataSavePath))
206 File.Delete(characterDataSavePath);
210 DebugConsole.ThrowError(
"ERROR: deleting character data file \"" + characterDataSavePath +
"\" failed.", e);
216 public static string GetSaveFolder(SaveType saveType)
218 string folder =
string.Empty;
220 if (!
string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath))
222 folder = GameSettings.CurrentConfig.SavePath;
223 if (saveType == SaveType.Multiplayer)
225 folder = Path.Combine(folder,
"Multiplayer");
227 if (!Directory.Exists(folder))
229 DebugConsole.AddWarning($
"Could not find the custom save folder \"{folder}\", creating the folder...");
232 Directory.CreateDirectory(folder);
236 DebugConsole.ThrowError($
"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e);
237 folder =
string.Empty;
241 if (
string.IsNullOrEmpty(folder))
243 folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
248 public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType,
bool includeInCompatible =
true,
bool logLoadErrors =
true)
250 string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
251 if (!Directory.Exists(defaultFolder))
253 DebugConsole.Log(
"Save folder \"" + defaultFolder +
" not found! Attempting to create a new folder...");
256 Directory.CreateDirectory(defaultFolder);
260 DebugConsole.ThrowError(
"Failed to create the folder \"" + defaultFolder +
"\"!", e);
264 List<string> files = Directory.GetFiles(defaultFolder,
"*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
266 var folder = GetSaveFolder(saveType);
267 if (!
string.IsNullOrEmpty(folder) && Directory.Exists(folder))
269 files.AddRange(Directory.GetFiles(folder,
"*.save", System.IO.SearchOption.TopDirectoryOnly));
272 string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder;
273 if (Directory.Exists(legacyFolder))
275 files.AddRange(Directory.GetFiles(legacyFolder,
"*.save", System.IO.SearchOption.TopDirectoryOnly));
278 files = files.Distinct().ToList();
280 List<CampaignMode.SaveInfo> saveInfos =
new List<CampaignMode.SaveInfo>();
281 foreach (
string file
in files)
283 var docRoot = ExtractGameSessionRootElementFromSaveFile(file, logLoadErrors);
284 if (!includeInCompatible && !IsSaveFileCompatible(docRoot))
290 saveInfos.Add(
new CampaignMode.SaveInfo(
292 SaveTime: Option.None,
294 EnabledContentPackageNames: ImmutableArray<string>.Empty));
298 List<string> enabledContentPackageNames =
new List<string>();
301 string enabledContentPackagePathsStr = docRoot.GetAttributeStringUnrestricted(
"selectedcontentpackages",
string.Empty);
302 foreach (
string packagePath
in enabledContentPackagePathsStr.Split(
'|'))
304 if (
string.IsNullOrEmpty(packagePath)) {
continue; }
306 string fileName = Path.GetFileNameWithoutExtension(packagePath);
307 if (fileName ==
"filelist")
309 enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath) ??
""));
313 enabledContentPackageNames.Add(fileName);
317 string enabledContentPackageNamesStr = docRoot.GetAttributeStringUnrestricted(
"selectedcontentpackagenames",
string.Empty);
321 if (
string.IsNullOrEmpty(packageName)) {
continue; }
322 enabledContentPackageNames.Add(packageName.Replace(
@"\|",
"|"));
325 saveInfos.Add(
new CampaignMode.SaveInfo(
327 SaveTime: docRoot.GetAttributeDateTime(
"savetime"),
328 SubmarineName: docRoot.GetAttributeStringUnrestricted(
"submarine",
""),
329 EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
336 public static string CreateSavePath(SaveType saveType,
string fileName =
"Save_Default")
338 fileName = ToolBox.RemoveInvalidFileNameChars(fileName);
340 string folder = GetSaveFolder(saveType);
341 if (fileName ==
"Save_Default")
343 fileName = TextManager.Get(
"SaveFile.DefaultName").Value;
344 if (fileName.Length == 0) fileName =
"Save";
347 if (!Directory.Exists(folder))
349 DebugConsole.Log(
"Save folder \"" + folder +
"\" not found. Created new folder");
350 Directory.CreateDirectory(folder);
353 string extension =
".save";
354 string pathWithoutExtension = Path.Combine(folder, fileName);
356 if (!File.Exists(pathWithoutExtension + extension))
358 return pathWithoutExtension + extension;
362 while (File.Exists(pathWithoutExtension +
" " + i + extension))
367 return pathWithoutExtension +
" " + i + extension;
370 public static void CompressStringToFile(
string fileName,
string value)
374 byte[] b = Encoding.UTF8.GetBytes(value);
378 using FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)
379 ??
throw new Exception($
"Failed to create file \"{fileName}\"");;
380 using GZipStream gz =
new GZipStream(f2, CompressionMode.Compress,
false);
381 gz.Write(b, 0, b.Length);
384 private static void CompressFile(
string sDir,
string sRelativePath, GZipStream zipStream)
387 if (sRelativePath.Length > 255)
390 $
"Failed to compress \"{sDir}\" (file name length > 255).");
393 zipStream.WriteByte((
byte)sRelativePath.Length);
394 zipStream.WriteByte(0);
395 zipStream.WriteByte(0);
396 zipStream.WriteByte(0);
398 var strBytes = Encoding.Unicode.GetBytes(sRelativePath.CleanUpPathCrossPlatform(correctFilenameCase:
false));
399 zipStream.Write(strBytes, 0, strBytes.Length);
402 byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath));
403 zipStream.Write(BitConverter.GetBytes(bytes.Length), 0,
sizeof(
int));
404 zipStream.Write(bytes, 0, bytes.Length);
407 public static void CompressDirectory(
string sInDir,
string sOutFile)
409 IEnumerable<string> sFiles = Directory.GetFiles(sInDir,
"*.*", System.IO.SearchOption.AllDirectories);
410 int iDirLen = sInDir[^1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1;
412 using var outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)
413 ??
throw new Exception($
"Failed to create file \"{sOutFile}\"");
414 using GZipStream str =
new GZipStream(outFile, CompressionMode.Compress);
415 foreach (
string sFilePath
in sFiles)
417 string sRelativePath = sFilePath.Substring(iDirLen);
418 CompressFile(sInDir, sRelativePath, str);
423 public static System.IO.Stream DecompressFileToStream(
string fileName)
425 using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read)
426 ??
throw new Exception($
"Failed to open file \"{fileName}\"");
427 System.IO.MemoryStream streamToReturn =
new System.IO.MemoryStream();
429 using GZipStream gzipStream =
new GZipStream(originalFileStream, CompressionMode.Decompress);
430 gzipStream.CopyTo(streamToReturn);
432 streamToReturn.Position = 0;
433 return streamToReturn;
436 private static bool IsExtractionPathValid(
string rootDir,
string fileDir)
438 string getFullPath(
string dir)
439 => (
string.IsNullOrEmpty(dir)
440 ? Directory.GetCurrentDirectory()
441 : Path.GetFullPath(dir))
442 .CleanUpPathCrossPlatform(correctFilenameCase:
false);
444 string rootDirFull = getFullPath(rootDir);
445 string fileDirFull = getFullPath(fileDir);
447 return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase);
450 private static bool DecompressFile(System.IO.BinaryReader reader, [NotNullWhen(returnValue:
true)]out
string? fileName, [NotNullWhen(returnValue:
true)]out
byte[]? fileContent)
455 if (reader.PeekChar() < 0) {
return false; }
458 int nameLen = reader.ReadInt32();
462 $
"Failed to decompress (file name length > 255). The file may be corrupted.");
465 byte[] strBytes = reader.ReadBytes(nameLen *
sizeof(
char));
466 string sFileName = Encoding.Unicode.GetString(strBytes)
469 fileName = sFileName;
472 int contentLen = reader.ReadInt32();
473 fileContent = reader.ReadBytes(contentLen);
478 public static void DecompressToDirectory(
string sCompressedFile,
string sDir)
480 DebugConsole.Log(
"Decompressing " + sCompressedFile +
" to " + sDir +
"...");
481 const int maxRetries = 4;
482 for (
int i = 0; i <= maxRetries; i++)
486 using var memStream = DecompressFileToStream(sCompressedFile);
487 using var reader =
new System.IO.BinaryReader(memStream);
488 while (DecompressFile(reader, out var fileName, out var contentBytes))
490 string sFilePath = Path.Combine(sDir, fileName);
491 string sFinalDir = Path.GetDirectoryName(sFilePath) ??
"";
493 if (!IsExtractionPathValid(sDir, sFinalDir))
495 throw new InvalidOperationException(
496 $
"Error extracting \"{fileName}\": cannot be extracted to parent directory");
499 Directory.CreateDirectory(sFinalDir);
500 using var outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)
501 ??
throw new Exception($
"Failed to create file \"{sFilePath}\"");
502 outFile.Write(contentBytes, 0, contentBytes.Length);
506 catch (System.IO.IOException e)
508 if (i >= maxRetries || !File.Exists(sCompressedFile)) {
throw; }
509 DebugConsole.NewMessage(
"Failed decompress file \"" + sCompressedFile +
"\" {" + e.Message +
"}, retrying in 250 ms...", Color.Red);
515 public static IEnumerable<string> EnumerateContainedFiles(
string sCompressedFile)
517 const int maxRetries = 4;
518 HashSet<string> paths =
new HashSet<string>();
519 for (
int i = 0; i <= maxRetries; i++)
524 using var memStream = DecompressFileToStream(sCompressedFile);
525 using var reader =
new System.IO.BinaryReader(memStream);
526 while (DecompressFile(reader, out var fileName, out _))
532 catch (System.IO.IOException e)
534 if (i >= maxRetries || !File.Exists(sCompressedFile)) {
throw; }
536 DebugConsole.NewMessage(
537 $
"Failed to decompress file \"{sCompressedFile}\" for enumeration {{{e.Message}}}, retrying in 250 ms...",
552 public static XDocument? DecompressSaveAndLoadGameSessionDoc(
string savePath)
554 DebugConsole.Log(
"Loading game session doc: " + savePath);
557 DecompressToDirectory(savePath, TempPath);
561 DebugConsole.ThrowError(
"Error decompressing " + savePath, e);
564 return XMLExtensions.TryLoadXml(Path.Combine(TempPath,
"gamesession.xml"));
571 public static XElement? ExtractGameSessionRootElementFromSaveFile(
string savePath,
bool logLoadErrors =
true)
573 const int maxRetries = 4;
574 for (
int i = 0; i <= maxRetries; i++)
578 using var memStream = DecompressFileToStream(savePath);
579 using var reader =
new System.IO.BinaryReader(memStream);
580 while (DecompressFile(reader, out var fileName, out var fileContent))
582 if (fileName != GameSessionFileName) {
continue; }
585 int tagOpenerStartIndex = -1;
586 for (
int j = 0; j < fileContent.Length; j++)
588 if (fileContent[j] ==
'<')
591 if (tagOpenerStartIndex >= 0) {
return null; }
592 tagOpenerStartIndex = j;
594 else if (j > 0 && fileContent[j] ==
'?' && fileContent[j - 1] ==
'<')
597 tagOpenerStartIndex = -1;
599 else if (fileContent[j] ==
'>')
602 if (tagOpenerStartIndex < 0) {
continue; }
604 string elemStr = Encoding.UTF8.GetString(fileContent.AsSpan()[tagOpenerStartIndex..j]) +
"/>";
607 return XElement.Parse(elemStr);
611 DebugConsole.NewMessage(
612 $
"Failed to parse gamesession root in \"{savePath}\": {{{e.Message}}}.",
622 catch (System.IO.IOException e)
624 if (i >= maxRetries || !File.Exists(savePath)) {
throw; }
626 DebugConsole.NewMessage(
627 $
"Failed to decompress file \"{savePath}\" for root extraction ({e.Message}), retrying in 250 ms...",
631 catch (System.IO.InvalidDataException e)
635 DebugConsole.ThrowError($
"Failed to decompress file \"{savePath}\" for root extraction.", e);
643 public static void DeleteDownloadedSubs()
645 if (Directory.Exists(SubmarineDownloadFolder))
647 ClearFolder(SubmarineDownloadFolder);
651 public static void CleanUnnecessarySaveFiles()
653 if (Directory.Exists(CampaignDownloadFolder))
655 ClearFolder(CampaignDownloadFolder);
656 Directory.Delete(CampaignDownloadFolder);
658 if (Directory.Exists(TempPath))
660 ClearFolder(TempPath);
661 Directory.Delete(TempPath);
665 public static void ClearFolder(
string folderName,
string[]? ignoredFileNames =
null)
671 if (ignoredFileNames !=
null)
674 foreach (
string ignoredFile
in ignoredFileNames)
676 if (Path.GetFileName(fi.
FullName).Equals(Path.GetFileName(ignoredFile)))
682 if (ignore)
continue;
690 ClearFolder(di.
FullName, ignoredFileNames);
691 const int maxRetries = 4;
692 for (
int i = 0; i <= maxRetries; i++)
699 catch (System.IO.IOException)
701 if (i >= maxRetries) {
throw; }
IEnumerable< FileInfo > GetFiles()
IEnumerable< DirectoryInfo > GetDirectories()