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;
26 throw new ArgumentException(
"Save path cannot be a backup path.", nameof(savePath));
46 string extension = Path.GetExtension(path);
47 bool startsWith = extension.StartsWith(SaveUtil.BackupExtension, StringComparison.OrdinalIgnoreCase);
55 bool hasIndex = SaveUtil.TryGetBackupIndexFromFileName(path, out foundIndex);
62 public const string GameSessionFileName =
"gamesession.xml";
64 private static readonly
string LegacySaveFolder = Path.Combine(
"Data",
"Saves");
65 private static readonly
string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder,
"Multiplayer");
69 public static readonly
string DefaultSaveFolder = Path.Combine(
70 Environment.GetFolderPath(Environment.SpecialFolder.Personal),
72 "Application Support",
73 "Daedalic Entertainment GmbH",
78 public static readonly
string DefaultSaveFolder = Path.Combine(
79 Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
80 "Daedalic Entertainment GmbH",
84 public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder,
"Multiplayer");
86 public static readonly
string SubmarineDownloadFolder = Path.Combine(
"Submarines",
"Downloaded");
87 public static readonly
string CampaignDownloadFolder = Path.Combine(
"Data",
"Saves",
"Multiplayer_Downloaded");
89 public const string BackupExtension =
".bk";
94 public const string FullBackupExtension = $
".save{BackupExtension}";
99 public const string BackupExtensionFormat = $
"{FullBackupExtension}{{0}}";
104 public const string BackupCharacterDataExtensionStart = $
".xml{BackupExtension}";
109 public const string BackupCharacterDataFormat = $
"{BackupCharacterDataExtensionStart}{{0}}";
111 public static int MaxBackupCount = 3;
113 public static string TempPath
116 get {
return Path.Combine(GetSaveFolder(SaveType.Singleplayer),
"temp_server"); }
118 get {
return Path.Combine(GetSaveFolder(SaveType.Singleplayer),
"temp"); }
137 public static void SaveGame(CampaignDataPath filePath,
bool isSavingOnLoading =
false)
139 if (!isSavingOnLoading && File.Exists(filePath.SavePath))
141 BackupSave(filePath.SavePath);
144 DebugConsole.Log(
"Saving the game to: " + filePath);
145 Directory.CreateDirectory(TempPath, catchUnauthorizedAccessExceptions:
true);
148 ClearFolder(TempPath,
new string[] { GameMain.GameSession.SubmarineInfo.FilePath });
152 DebugConsole.ThrowError(
"Failed to clear folder", e);
158 GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName), isSavingOnLoading);
162 DebugConsole.ThrowError(
"Error saving gamesession", e);
168 string? mainSubPath =
null;
169 if (GameMain.GameSession.SubmarineInfo !=
null)
171 mainSubPath = Path.Combine(TempPath, GameMain.GameSession.SubmarineInfo.Name +
".sub");
172 GameMain.GameSession.SubmarineInfo.SaveAs(mainSubPath);
173 for (
int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
175 if (GameMain.GameSession.OwnedSubmarines[i].Name == GameMain.GameSession.SubmarineInfo.Name)
177 GameMain.GameSession.OwnedSubmarines[i] = GameMain.GameSession.SubmarineInfo;
182 if (GameMain.GameSession.OwnedSubmarines !=
null)
184 for (
int i = 0; i < GameMain.GameSession.OwnedSubmarines.Count; i++)
186 SubmarineInfo storedInfo = GameMain.GameSession.OwnedSubmarines[i];
187 string subPath = Path.Combine(TempPath, storedInfo.Name +
".sub");
188 if (mainSubPath == subPath) {
continue; }
189 storedInfo.SaveAs(subPath);
195 DebugConsole.ThrowError(
"Error saving submarine", e);
201 CompressDirectory(TempPath, filePath.SavePath);
205 DebugConsole.ThrowError(
"Error compressing save file", e);
209 public static void LoadGame(CampaignDataPath path)
214 GameMain.GameSession =
null;
215 DebugConsole.Log(
"Loading save file: " + path.LoadPath);
216 DecompressToDirectory(path.LoadPath, TempPath);
218 XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName));
219 if (doc ==
null) {
return; }
221 if (!IsSaveFileCompatible(doc))
223 throw new Exception($
"The save file \"{path.LoadPath}\" is not compatible with this version of Barotrauma.");
226 var ownedSubmarines = LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub);
227 GameMain.GameSession =
new GameSession(selectedSub, ownedSubmarines, doc, path);
230 public static List<SubmarineInfo> LoadOwnedSubmarines(XDocument saveDoc, out SubmarineInfo selectedSub)
232 string subPath = Path.Combine(TempPath, saveDoc.Root.GetAttributeString(
"submarine",
"")) +
".sub";
233 selectedSub =
new SubmarineInfo(subPath);
235 List<SubmarineInfo> ownedSubmarines =
new List<SubmarineInfo>();
237 var ownedSubsElement = saveDoc.Root?.Element(
"ownedsubmarines");
238 if (ownedSubsElement ==
null) {
return ownedSubmarines; }
240 foreach (var subElement
in ownedSubsElement.Elements())
242 string subName = subElement.GetAttributeString(
"name",
"");
243 string ownedSubPath = Path.Combine(TempPath, subName +
".sub");
244 if (!File.Exists(ownedSubPath))
246 DebugConsole.ThrowError($
"Could not find the submarine \"{subName}\" ({ownedSubPath})! The save file may be corrupted. Removing the submarine from owned submarines...");
250 ownedSubmarines.Add(
new SubmarineInfo(ownedSubPath));
253 return ownedSubmarines;
256 public static bool IsSaveFileCompatible(XDocument? saveDoc)
257 => IsSaveFileCompatible(saveDoc?.Root);
259 public static bool IsSaveFileCompatible(XElement? saveDocRoot)
261 if (saveDocRoot?.Attribute(
"version") ==
null) {
return false; }
265 public static void DeleteSave(
string filePath)
269 File.Delete(filePath, catchUnauthorizedAccessExceptions:
false);
271 string[] backups = GetBackupPaths(Path.GetDirectoryName(filePath) ??
"", Path.GetFileNameWithoutExtension(filePath));
272 foreach (
string backup
in backups)
274 File.Delete(backup, catchUnauthorizedAccessExceptions:
false);
279 DebugConsole.ThrowError(
"ERROR: deleting save file \"" + filePath +
"\" failed.", e);
283 var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath) ??
"");
285 if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) ||
286 fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer)))
288 string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath);
289 if (File.Exists(characterDataSavePath))
293 File.Delete(characterDataSavePath, catchUnauthorizedAccessExceptions:
false);
297 DebugConsole.ThrowError(
"ERROR: deleting character data file \"" + characterDataSavePath +
"\" failed.", e);
303 public static string GetSaveFolder(SaveType saveType)
305 string folder =
string.Empty;
307 if (!
string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath))
309 folder = GameSettings.CurrentConfig.SavePath;
310 if (saveType == SaveType.Multiplayer)
312 folder = Path.Combine(folder,
"Multiplayer");
314 if (!Directory.Exists(folder))
316 DebugConsole.AddWarning($
"Could not find the custom save folder \"{folder}\", creating the folder...");
319 Directory.CreateDirectory(folder, catchUnauthorizedAccessExceptions:
false);
323 DebugConsole.ThrowError($
"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e);
324 folder =
string.Empty;
328 if (
string.IsNullOrEmpty(folder))
330 folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
335 public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType,
bool includeInCompatible =
true,
bool logLoadErrors =
true)
337 string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
338 if (!Directory.Exists(defaultFolder))
340 DebugConsole.Log(
"Save folder \"" + defaultFolder +
" not found! Attempting to create a new folder...");
343 Directory.CreateDirectory(defaultFolder, catchUnauthorizedAccessExceptions:
false);
347 DebugConsole.ThrowError(
"Failed to create the folder \"" + defaultFolder +
"\"!", e);
351 List<string> files = Directory.GetFiles(defaultFolder,
"*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
353 var folder = GetSaveFolder(saveType);
354 if (!
string.IsNullOrEmpty(folder) && Directory.Exists(folder))
356 files.AddRange(Directory.GetFiles(folder,
"*.save", System.IO.SearchOption.TopDirectoryOnly));
359 string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder;
360 if (Directory.Exists(legacyFolder))
362 files.AddRange(Directory.GetFiles(legacyFolder,
"*.save", System.IO.SearchOption.TopDirectoryOnly));
365 files = files.Distinct().ToList();
367 List<CampaignMode.SaveInfo> saveInfos =
new List<CampaignMode.SaveInfo>();
368 foreach (
string file
in files)
370 var docRoot = ExtractGameSessionRootElementFromSaveFile(file, logLoadErrors);
371 if (!includeInCompatible && !IsSaveFileCompatible(docRoot))
377 saveInfos.Add(
new CampaignMode.SaveInfo(
379 SaveTime: Option.None,
381 EnabledContentPackageNames: ImmutableArray<string>.Empty));
385 List<string> enabledContentPackageNames =
new List<string>();
388 string enabledContentPackagePathsStr = docRoot.GetAttributeStringUnrestricted(
"selectedcontentpackages",
string.Empty);
389 foreach (
string packagePath
in enabledContentPackagePathsStr.Split(
'|'))
391 if (
string.IsNullOrEmpty(packagePath)) {
continue; }
393 string fileName = Path.GetFileNameWithoutExtension(packagePath);
394 if (fileName ==
"filelist")
396 enabledContentPackageNames.Add(Path.GetFileName(Path.GetDirectoryName(packagePath) ??
""));
400 enabledContentPackageNames.Add(fileName);
404 string enabledContentPackageNamesStr = docRoot.GetAttributeStringUnrestricted(
"selectedcontentpackagenames",
string.Empty);
408 if (
string.IsNullOrEmpty(packageName)) {
continue; }
409 enabledContentPackageNames.Add(packageName.Replace(
@"\|",
"|"));
412 saveInfos.Add(
new CampaignMode.SaveInfo(
414 SaveTime: docRoot.GetAttributeDateTime(
"savetime"),
415 SubmarineName: docRoot.GetAttributeStringUnrestricted(
"submarine",
""),
416 EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
423 public static string CreateSavePath(SaveType saveType,
string fileName =
"Save_Default")
425 fileName = ToolBox.RemoveInvalidFileNameChars(fileName);
427 string folder = GetSaveFolder(saveType);
428 if (fileName ==
"Save_Default")
430 fileName = TextManager.Get(
"SaveFile.DefaultName").Value;
431 if (fileName.Length == 0) fileName =
"Save";
434 if (!Directory.Exists(folder))
436 DebugConsole.Log(
"Save folder \"" + folder +
"\" not found. Created new folder");
437 Directory.CreateDirectory(folder, catchUnauthorizedAccessExceptions:
true);
440 string extension =
".save";
441 string pathWithoutExtension = Path.Combine(folder, fileName);
443 if (!File.Exists(pathWithoutExtension + extension))
445 return pathWithoutExtension + extension;
449 while (File.Exists(pathWithoutExtension +
" " + i + extension))
454 return pathWithoutExtension +
" " + i + extension;
457 public static void CompressStringToFile(
string fileName,
string value)
461 byte[] b = Encoding.UTF8.GetBytes(value);
465 using FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)
466 ??
throw new Exception($
"Failed to create file \"{fileName}\"");;
467 using GZipStream gz =
new GZipStream(f2, CompressionMode.Compress,
false);
468 gz.Write(b, 0, b.Length);
471 private static void CompressFile(
string sDir,
string sRelativePath, GZipStream zipStream)
474 if (sRelativePath.Length > 255)
477 $
"Failed to compress \"{sDir}\" (file name length > 255).");
480 zipStream.WriteByte((
byte)sRelativePath.Length);
481 zipStream.WriteByte(0);
482 zipStream.WriteByte(0);
483 zipStream.WriteByte(0);
485 var strBytes = Encoding.Unicode.GetBytes(sRelativePath.CleanUpPathCrossPlatform(correctFilenameCase:
false));
486 zipStream.Write(strBytes, 0, strBytes.Length);
489 byte[] bytes = File.ReadAllBytes(Path.Combine(sDir, sRelativePath));
490 zipStream.Write(BitConverter.GetBytes(bytes.Length), 0,
sizeof(
int));
491 zipStream.Write(bytes, 0, bytes.Length);
494 public static void CompressDirectory(
string sInDir,
string sOutFile)
496 IEnumerable<string> sFiles = Directory.GetFiles(sInDir,
"*.*", System.IO.SearchOption.AllDirectories);
497 int iDirLen = sInDir[^1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1;
499 using var outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)
500 ??
throw new Exception($
"Failed to create file \"{sOutFile}\"");
501 using GZipStream str =
new GZipStream(outFile, CompressionMode.Compress);
502 foreach (
string sFilePath
in sFiles)
504 string sRelativePath = sFilePath.Substring(iDirLen);
505 CompressFile(sInDir, sRelativePath, str);
510 public static System.IO.Stream DecompressFileToStream(
string fileName)
512 using FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open, System.IO.FileAccess.Read)
513 ??
throw new Exception($
"Failed to open file \"{fileName}\"");
514 System.IO.MemoryStream streamToReturn =
new System.IO.MemoryStream();
516 using GZipStream gzipStream =
new GZipStream(originalFileStream, CompressionMode.Decompress);
517 gzipStream.CopyTo(streamToReturn);
519 streamToReturn.Position = 0;
520 return streamToReturn;
523 private static bool IsExtractionPathValid(
string rootDir,
string fileDir)
525 string getFullPath(
string dir)
526 => (
string.IsNullOrEmpty(dir)
527 ? Directory.GetCurrentDirectory()
528 : Path.GetFullPath(dir))
529 .CleanUpPathCrossPlatform(correctFilenameCase:
false);
531 string rootDirFull = getFullPath(rootDir);
532 string fileDirFull = getFullPath(fileDir);
534 return fileDirFull.StartsWith(rootDirFull, StringComparison.OrdinalIgnoreCase);
537 private static bool DecompressFile(System.IO.BinaryReader reader, [NotNullWhen(returnValue:
true)]out
string? fileName, [NotNullWhen(returnValue:
true)]out
byte[]? fileContent)
542 if (reader.PeekChar() < 0) {
return false; }
545 int nameLen = reader.ReadInt32();
549 $
"Failed to decompress (file name length > 255). The file may be corrupted.");
552 byte[] strBytes = reader.ReadBytes(nameLen *
sizeof(
char));
553 string sFileName = Encoding.Unicode.GetString(strBytes)
556 fileName = sFileName;
559 int contentLen = reader.ReadInt32();
560 fileContent = reader.ReadBytes(contentLen);
565 public static void DecompressToDirectory(
string sCompressedFile,
string sDir)
567 DebugConsole.Log(
"Decompressing " + sCompressedFile +
" to " + sDir +
"...");
568 const int maxRetries = 4;
569 for (
int i = 0; i <= maxRetries; i++)
573 using var memStream = DecompressFileToStream(sCompressedFile);
574 using var reader =
new System.IO.BinaryReader(memStream);
575 while (DecompressFile(reader, out var fileName, out var contentBytes))
577 string sFilePath = Path.Combine(sDir, fileName);
578 string sFinalDir = Path.GetDirectoryName(sFilePath) ??
"";
580 if (!IsExtractionPathValid(sDir, sFinalDir))
582 throw new InvalidOperationException(
583 $
"Error extracting \"{fileName}\": cannot be extracted to parent directory");
586 Directory.CreateDirectory(sFinalDir);
587 using var outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)
588 ??
throw new Exception($
"Failed to create file \"{sFilePath}\"");
589 outFile.Write(contentBytes, 0, contentBytes.Length);
593 catch (System.IO.IOException e)
595 if (i >= maxRetries || !File.Exists(sCompressedFile)) {
throw; }
596 DebugConsole.NewMessage(
"Failed decompress file \"" + sCompressedFile +
"\" {" + e.Message +
"}, retrying in 250 ms...", Color.Red);
602 public static IEnumerable<string> EnumerateContainedFiles(
string sCompressedFile)
604 const int maxRetries = 4;
605 HashSet<string> paths =
new HashSet<string>();
606 for (
int i = 0; i <= maxRetries; i++)
611 using var memStream = DecompressFileToStream(sCompressedFile);
612 using var reader =
new System.IO.BinaryReader(memStream);
613 while (DecompressFile(reader, out var fileName, out _))
619 catch (System.IO.IOException e)
621 if (i >= maxRetries || !File.Exists(sCompressedFile)) {
throw; }
623 DebugConsole.NewMessage(
624 $
"Failed to decompress file \"{sCompressedFile}\" for enumeration {{{e.Message}}}, retrying in 250 ms...",
639 public static XDocument? DecompressSaveAndLoadGameSessionDoc(
string savePath)
641 DebugConsole.Log(
"Loading game session doc: " + savePath);
644 DecompressToDirectory(savePath, TempPath);
648 DebugConsole.ThrowError(
"Error decompressing " + savePath, e);
651 return XMLExtensions.TryLoadXml(Path.Combine(TempPath,
"gamesession.xml"));
658 public static XElement? ExtractGameSessionRootElementFromSaveFile(
string savePath,
bool logLoadErrors =
true)
660 const int maxRetries = 4;
661 for (
int i = 0; i <= maxRetries; i++)
665 using var memStream = DecompressFileToStream(savePath);
666 using var reader =
new System.IO.BinaryReader(memStream);
667 while (DecompressFile(reader, out var fileName, out var fileContent))
669 if (fileName != GameSessionFileName) {
continue; }
672 int tagOpenerStartIndex = -1;
673 for (
int j = 0; j < fileContent.Length; j++)
675 if (fileContent[j] ==
'<')
678 if (tagOpenerStartIndex >= 0) {
return null; }
679 tagOpenerStartIndex = j;
681 else if (j > 0 && fileContent[j] ==
'?' && fileContent[j - 1] ==
'<')
684 tagOpenerStartIndex = -1;
686 else if (fileContent[j] ==
'>')
689 if (tagOpenerStartIndex < 0) {
continue; }
691 string elemStr = Encoding.UTF8.GetString(fileContent.AsSpan()[tagOpenerStartIndex..j]) +
"/>";
694 return XElement.Parse(elemStr);
698 DebugConsole.NewMessage(
699 $
"Failed to parse gamesession root in \"{savePath}\": {{{e.Message}}}.",
709 catch (System.IO.IOException e)
711 if (i >= maxRetries || !File.Exists(savePath)) {
throw; }
713 DebugConsole.NewMessage(
714 $
"Failed to decompress file \"{savePath}\" for root extraction ({e.Message}), retrying in 250 ms...",
718 catch (System.IO.InvalidDataException e)
722 DebugConsole.ThrowError($
"Failed to decompress file \"{savePath}\" for root extraction.", e);
730 public static void DeleteDownloadedSubs()
732 if (Directory.Exists(SubmarineDownloadFolder))
734 ClearFolder(SubmarineDownloadFolder);
738 public static void CleanUnnecessarySaveFiles()
740 if (Directory.Exists(CampaignDownloadFolder))
742 ClearFolder(CampaignDownloadFolder);
743 Directory.Delete(CampaignDownloadFolder);
745 if (Directory.Exists(TempPath))
747 ClearFolder(TempPath);
748 Directory.Delete(TempPath);
752 public static void ClearFolder(
string folderName,
string[]? ignoredFileNames =
null)
758 if (ignoredFileNames !=
null)
761 foreach (
string ignoredFile
in ignoredFileNames)
763 if (Path.GetFileName(fi.
FullName).Equals(Path.GetFileName(ignoredFile)))
769 if (ignore)
continue;
777 ClearFolder(di.
FullName, ignoredFileNames);
778 const int maxRetries = 4;
779 for (
int i = 0; i <= maxRetries; i++)
786 catch (System.IO.IOException)
788 if (i >= maxRetries) {
throw; }
798 public readonly record
struct BackupIndexData(uint Index,
799 Identifier LocationNameIdentifier,
800 int LocationNameFormatIndex,
801 Identifier LocationType,
802 LevelData.LevelType LevelType,
803 SerializableDateTime SaveTime) : INetSerializableStruct;
805 public static string FormatBackupExtension(uint index) => string.Format(BackupExtensionFormat, index);
806 public static string FormatBackupCharacterDataExtension(uint index) => string.Format(BackupCharacterDataFormat, index);
808 public static void BackupSave(string savePath)
810 string path = Path.GetDirectoryName(savePath) ??
"";
811 string fileName = Path.GetFileNameWithoutExtension(savePath);
812 string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(savePath);
813 string characterDataFileName = Path.GetFileNameWithoutExtension(characterDataSavePath);
815 ImmutableArray<BackupIndexData> indexData = GetIndexData(path, fileName);
817 uint freeIndex = GetFreeIndex(indexData);
819 string newBackupPath = Path.Combine(path, $
".{fileName}{FormatBackupExtension(freeIndex)}");
820 string newCharacterDataBackupPath = Path.Combine(path, $
".{characterDataFileName}{FormatBackupCharacterDataExtension(freeIndex)}");
824 BackupFile(savePath, newBackupPath);
825 if (File.Exists(characterDataSavePath))
827 BackupFile(characterDataSavePath, newCharacterDataBackupPath);
832 DebugConsole.ThrowError(
"Failed to create a backup of the save file.", e);
835 static uint GetFreeIndex(IEnumerable<BackupIndexData> indexData)
837 if (!indexData.Any()) {
return 0; }
839 if (indexData.Count() < MaxBackupCount)
841 uint highestIndex = indexData.Max(
static b => b.Index);
842 uint nextIndex = highestIndex + 1;
844 if (indexData.Any(b => b.Index == nextIndex))
846 for (uint i = 0; i < MaxBackupCount; i++)
848 if (indexData.All(b => b.Index != i)) {
return i; }
852 throw new InvalidOperationException(
"Failed to find a free index for the backup.");
858 BackupIndexData oldestBackup = indexData.OrderBy(
static b => b.SaveTime).First();
859 return oldestBackup.Index;
862 static void BackupFile(
string sourcePath,
string destPath)
865 DeleteIfExists(destPath);
866 System.IO.File.Copy(sourcePath, destPath, overwrite:
true);
871 public static void DeleteIfExists(
string filePath)
873 if (System.IO.File.Exists(filePath))
875 System.IO.File.Delete(filePath);
879 public static ImmutableArray<BackupIndexData> GetIndexData(
string fullPath)
881 string path = Path.GetDirectoryName(fullPath) ??
"";
882 string fileName = Path.GetFileNameWithoutExtension(fullPath);
883 return GetIndexData(path, fileName);
886 private static readonly System.IO.EnumerationOptions BackupEnumerationOptions =
new System.IO.EnumerationOptions
888 MatchType = System.IO.MatchType.Win32,
889 AttributesToSkip = System.IO.FileAttributes.System,
890 IgnoreInaccessible =
true
893 private static string[] GetBackupPaths(
string path,
string baseName)
897 return System.IO.Directory.GetFiles(path, $
".{baseName}{FullBackupExtension}*", BackupEnumerationOptions);
901 DebugConsole.ThrowError(
"Failed to get backup paths.", e);
903 return Array.Empty<
string>();
906 public static bool TryGetBackupIndexFromFileName(
string filePath, out uint index)
908 string extension = Path.GetExtension(filePath);
909 if (extension.Length < BackupExtension.Length)
911 DebugConsole.ThrowError($
"The file name \"{filePath}\" does not have a valid backup extension.");
916 string indexStr = extension[BackupExtension.Length..];
917 bool result = uint.TryParse(indexStr, out index);
920 DebugConsole.ThrowError($
"Failed to parse the backup index from the file name \"{filePath}\".");
926 private static ImmutableArray<BackupIndexData> GetIndexData(
string path,
string baseName)
928 var builder = ImmutableArray.CreateBuilder<BackupIndexData>();
930 string[] foundBackups = GetBackupPaths(path, baseName);
932 foreach (
string backupPath
in foundBackups)
934 if (!TryGetBackupIndexFromFileName(backupPath, out uint index)) {
continue; }
936 var gameSession = ExtractGameSessionRootElementFromSaveFile(backupPath, logLoadErrors:
false);
938 if (gameSession is
null)
940 DebugConsole.AddWarning($
"Failed to load gamesession root from \"{backupPath}\". Skipping this backup.");
944 SerializableDateTime saveTime =
945 gameSession.GetAttributeDateTime(
"savetime")
946 .Fallback(SerializableDateTime.FromUtcUnixTime(0L));
948 Identifier locationNameIdentifier = gameSession.GetAttributeIdentifier(
"currentlocation", Identifier.Empty);
949 int locationNameFormatIndex = gameSession.GetAttributeInt(
"currentlocationnameformatindex", -1);
950 Identifier locationType = gameSession.GetAttributeIdentifier(
"locationtype", Identifier.Empty);
952 LevelData.LevelType levelType = gameSession.GetAttributeEnum(
"nextleveltype", LevelData.LevelType.LocationConnection);
954 builder.Add(
new BackupIndexData(index, locationNameIdentifier, locationNameFormatIndex, locationType, levelType, saveTime));
957 return builder.ToImmutable();
960 public static string GetBackupPath(
string savePath, uint index)
962 string path = Path.GetDirectoryName(savePath) ??
"";
963 string fileName = Path.GetFileNameWithoutExtension(savePath);
964 return Path.Combine(path, $
".{fileName}{FormatBackupExtension(index)}");
967 private static void SetHidden(
string filePath)
971 System.IO.File.SetAttributes(filePath, System.IO.File.GetAttributes(filePath) | System.IO.FileAttributes.Hidden);
975 DebugConsole.ThrowError(
"Failed to set the backup file as hidden.", e);
IEnumerable< FileInfo > GetFiles()
IEnumerable< DirectoryInfo > GetDirectories()
static bool IsBackupPath(string path, out uint foundIndex)
static CampaignDataPath CreateRegular(string savePath)
Creates a CampaignDataPath with the same load and save path.
static readonly CampaignDataPath Empty
Empty path used for non-campaign game sessions.
CampaignDataPath(string loadPath, string savePath)