v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -14,6 +14,49 @@ using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
public readonly struct CampaignDataPath
{
public readonly string LoadPath;
public readonly string SavePath;
public CampaignDataPath(string loadPath, string savePath)
{
if (IsBackupPath(savePath, out _))
{
throw new ArgumentException("Save path cannot be a backup path.", nameof(savePath));
}
LoadPath = loadPath;
SavePath = savePath;
}
/// <summary>
/// Empty path used for non-campaign game sessions.
/// </summary>
public static readonly CampaignDataPath Empty = new CampaignDataPath(loadPath: string.Empty, savePath: string.Empty);
/// <summary>
/// Creates a CampaignDataPath with the same load and save path.
/// </summary>
public static CampaignDataPath CreateRegular(string savePath)
=> new CampaignDataPath(savePath, savePath);
public static bool IsBackupPath(string path, out uint foundIndex)
{
string extension = Path.GetExtension(path);
bool startsWith = extension.StartsWith(SaveUtil.BackupExtension, StringComparison.OrdinalIgnoreCase);
if (!startsWith)
{
foundIndex = 0;
return false;
}
bool hasIndex = SaveUtil.TryGetBackupIndexFromFileName(path, out foundIndex);
return hasIndex;
}
}
static class SaveUtil
{
public const string GameSessionFileName = "gamesession.xml";
@@ -43,6 +86,30 @@ namespace Barotrauma
public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded");
public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded");
public const string BackupExtension = ".bk";
/// <summary>
/// .save.bk
/// </summary>
public const string FullBackupExtension = $".save{BackupExtension}";
/// <summary>
/// .save.bk0
/// </summary>
public const string BackupExtensionFormat = $"{FullBackupExtension}{{0}}";
/// <summary>
/// .xml.bk
/// </summary>
public const string BackupCharacterDataExtensionStart = $".xml{BackupExtension}";
/// <summary>
/// .xml.bk0
/// </summary>
public const string BackupCharacterDataFormat = $"{BackupCharacterDataExtensionStart}{{0}}";
public static int MaxBackupCount = 3;
public static string TempPath
{
#if SERVER
@@ -58,10 +125,24 @@ namespace Barotrauma
Multiplayer
}
public static void SaveGame(string filePath)
/// <summary>
/// Saves the game to a file.
/// </summary>
/// <param name="filePath">The path to the save file. </param>
/// <param name="isSavingOnLoading">
/// Indicates if the save is happening during loading in multiplayer
/// to ensure the campaign ID matches the one in the save file.
/// Used to work around some quirks with the backup system.
/// </param>
public static void SaveGame(CampaignDataPath filePath, bool isSavingOnLoading = false)
{
if (!isSavingOnLoading && File.Exists(filePath.SavePath))
{
BackupSave(filePath.SavePath);
}
DebugConsole.Log("Saving the game to: " + filePath);
Directory.CreateDirectory(TempPath);
Directory.CreateDirectory(TempPath, catchUnauthorizedAccessExceptions: true);
try
{
ClearFolder(TempPath, new string[] { GameMain.GameSession.SubmarineInfo.FilePath });
@@ -74,7 +155,7 @@ namespace Barotrauma
try
{
GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName));
GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName), isSavingOnLoading);
}
catch (Exception e)
{
@@ -117,7 +198,7 @@ namespace Barotrauma
try
{
CompressDirectory(TempPath, filePath);
CompressDirectory(TempPath, filePath.SavePath);
}
catch (Exception e)
{
@@ -125,25 +206,25 @@ namespace Barotrauma
}
}
public static void LoadGame(string filePath)
public static void LoadGame(CampaignDataPath path)
{
//ensure there's no gamesession/sub loaded because it'd lead to issues when starting a new one (e.g. trying to determine which level to load based on the placement of the sub)
//can happen if a gamesession is interrupted ungracefully (exception during loading)
Submarine.Unload();
GameMain.GameSession = null;
DebugConsole.Log("Loading save file: " + filePath);
DecompressToDirectory(filePath, TempPath);
DebugConsole.Log("Loading save file: " + path.LoadPath);
DecompressToDirectory(path.LoadPath, TempPath);
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine(TempPath, GameSessionFileName));
if (doc == null) { return; }
if (!IsSaveFileCompatible(doc))
{
throw new Exception($"The save file \"{filePath}\" is not compatible with this version of Barotrauma.");
throw new Exception($"The save file \"{path.LoadPath}\" is not compatible with this version of Barotrauma.");
}
var ownedSubmarines = LoadOwnedSubmarines(doc, out SubmarineInfo selectedSub);
GameMain.GameSession = new GameSession(selectedSub, ownedSubmarines, doc, filePath);
GameMain.GameSession = new GameSession(selectedSub, ownedSubmarines, doc, path);
}
public static List<SubmarineInfo> LoadOwnedSubmarines(XDocument saveDoc, out SubmarineInfo selectedSub)
@@ -185,7 +266,13 @@ namespace Barotrauma
{
try
{
File.Delete(filePath);
File.Delete(filePath, catchUnauthorizedAccessExceptions: false);
string[] backups = GetBackupPaths(Path.GetDirectoryName(filePath) ?? "", Path.GetFileNameWithoutExtension(filePath));
foreach (string backup in backups)
{
File.Delete(backup, catchUnauthorizedAccessExceptions: false);
}
}
catch (Exception e)
{
@@ -203,7 +290,7 @@ namespace Barotrauma
{
try
{
File.Delete(characterDataSavePath);
File.Delete(characterDataSavePath, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -229,7 +316,7 @@ namespace Barotrauma
DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder...");
try
{
Directory.CreateDirectory(folder);
Directory.CreateDirectory(folder, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -253,7 +340,7 @@ namespace Barotrauma
DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder...");
try
{
Directory.CreateDirectory(defaultFolder);
Directory.CreateDirectory(defaultFolder, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -347,7 +434,7 @@ namespace Barotrauma
if (!Directory.Exists(folder))
{
DebugConsole.Log("Save folder \"" + folder + "\" not found. Created new folder");
Directory.CreateDirectory(folder);
Directory.CreateDirectory(folder, catchUnauthorizedAccessExceptions: true);
}
string extension = ".save";
@@ -704,5 +791,190 @@ namespace Barotrauma
}
}
}
#region Backup saves
[NetworkSerialize]
public readonly record struct BackupIndexData(uint Index,
Identifier LocationNameIdentifier,
int LocationNameFormatIndex,
Identifier LocationType,
LevelData.LevelType LevelType,
SerializableDateTime SaveTime) : INetSerializableStruct;
public static string FormatBackupExtension(uint index) => string.Format(BackupExtensionFormat, index);
public static string FormatBackupCharacterDataExtension(uint index) => string.Format(BackupCharacterDataFormat, index);
public static void BackupSave(string savePath)
{
string path = Path.GetDirectoryName(savePath) ?? "";
string fileName = Path.GetFileNameWithoutExtension(savePath);
string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(savePath);
string characterDataFileName = Path.GetFileNameWithoutExtension(characterDataSavePath);
ImmutableArray<BackupIndexData> indexData = GetIndexData(path, fileName);
uint freeIndex = GetFreeIndex(indexData);
string newBackupPath = Path.Combine(path, $".{fileName}{FormatBackupExtension(freeIndex)}");
string newCharacterDataBackupPath = Path.Combine(path, $".{characterDataFileName}{FormatBackupCharacterDataExtension(freeIndex)}");
try
{
BackupFile(savePath, newBackupPath);
if (File.Exists(characterDataSavePath))
{
BackupFile(characterDataSavePath, newCharacterDataBackupPath);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to create a backup of the save file.", e);
}
static uint GetFreeIndex(IEnumerable<BackupIndexData> indexData)
{
if (!indexData.Any()) { return 0; }
if (indexData.Count() < MaxBackupCount)
{
uint highestIndex = indexData.Max(static b => b.Index);
uint nextIndex = highestIndex + 1;
if (indexData.Any(b => b.Index == nextIndex))
{
for (uint i = 0; i < MaxBackupCount; i++)
{
if (indexData.All(b => b.Index != i)) { return i; }
}
// this should theoretically never happen
throw new InvalidOperationException("Failed to find a free index for the backup.");
}
return nextIndex;
}
BackupIndexData oldestBackup = indexData.OrderBy(static b => b.SaveTime).First();
return oldestBackup.Index;
}
static void BackupFile(string sourcePath, string destPath)
{
// Overwriting a file that is marked as hidden will cause an exception.
DeleteIfExists(destPath);
System.IO.File.Copy(sourcePath, destPath, overwrite: true);
SetHidden(destPath);
}
}
public static void DeleteIfExists(string filePath)
{
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
}
}
public static ImmutableArray<BackupIndexData> GetIndexData(string fullPath)
{
string path = Path.GetDirectoryName(fullPath) ?? "";
string fileName = Path.GetFileNameWithoutExtension(fullPath);
return GetIndexData(path, fileName);
}
private static readonly System.IO.EnumerationOptions BackupEnumerationOptions = new System.IO.EnumerationOptions
{
MatchType = System.IO.MatchType.Win32,
AttributesToSkip = System.IO.FileAttributes.System,
IgnoreInaccessible = true
};
private static string[] GetBackupPaths(string path, string baseName)
{
try
{
return System.IO.Directory.GetFiles(path, $".{baseName}{FullBackupExtension}*", BackupEnumerationOptions);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to get backup paths.", e);
}
return Array.Empty<string>();
}
public static bool TryGetBackupIndexFromFileName(string filePath, out uint index)
{
string extension = Path.GetExtension(filePath);
if (extension.Length < BackupExtension.Length)
{
DebugConsole.ThrowError($"The file name \"{filePath}\" does not have a valid backup extension.");
index = 0;
return false;
}
string indexStr = extension[BackupExtension.Length..];
bool result = uint.TryParse(indexStr, out index);
if (!result)
{
DebugConsole.ThrowError($"Failed to parse the backup index from the file name \"{filePath}\".");
}
return result;
}
private static ImmutableArray<BackupIndexData> GetIndexData(string path, string baseName)
{
var builder = ImmutableArray.CreateBuilder<BackupIndexData>();
string[] foundBackups = GetBackupPaths(path, baseName);
foreach (string backupPath in foundBackups)
{
if (!TryGetBackupIndexFromFileName(backupPath, out uint index)) { continue; }
var gameSession = ExtractGameSessionRootElementFromSaveFile(backupPath, logLoadErrors: false);
if (gameSession is null)
{
DebugConsole.AddWarning($"Failed to load gamesession root from \"{backupPath}\". Skipping this backup.");
continue;
}
SerializableDateTime saveTime =
gameSession.GetAttributeDateTime("savetime")
.Fallback(SerializableDateTime.FromUtcUnixTime(0L));
Identifier locationNameIdentifier = gameSession.GetAttributeIdentifier("currentlocation", Identifier.Empty);
int locationNameFormatIndex = gameSession.GetAttributeInt("currentlocationnameformatindex", -1);
Identifier locationType = gameSession.GetAttributeIdentifier("locationtype", Identifier.Empty);
LevelData.LevelType levelType = gameSession.GetAttributeEnum("nextleveltype", LevelData.LevelType.LocationConnection);
builder.Add(new BackupIndexData(index, locationNameIdentifier, locationNameFormatIndex, locationType, levelType, saveTime));
}
return builder.ToImmutable();
}
public static string GetBackupPath(string savePath, uint index)
{
string path = Path.GetDirectoryName(savePath) ?? "";
string fileName = Path.GetFileNameWithoutExtension(savePath);
return Path.Combine(path, $".{fileName}{FormatBackupExtension(index)}");
}
private static void SetHidden(string filePath)
{
try
{
System.IO.File.SetAttributes(filePath, System.IO.File.GetAttributes(filePath) | System.IO.FileAttributes.Hidden);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to set the backup file as hidden.", e);
}
}
#endregion
}
}