Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -2,10 +2,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Barotrauma.IO;
using Voronoi2;
namespace Barotrauma
@@ -51,16 +48,9 @@ namespace Barotrauma
public static int ThreadId = 0;
private static void CheckRandThreadSafety(RandSync sync)
{
if (ThreadId != 0 && sync == RandSync.Unsynced)
{
if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId)
{
Debug.WriteLine($"Unsynced rand used in synced thread! {Environment.StackTrace}");
}
}
if (ThreadId != 0 && sync == RandSync.ServerAndClient)
{
if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadId)
if (Environment.CurrentManagedThreadId != ThreadId)
{
#if DEBUG
throw new Exception("Unauthorized multithreaded access to RandSync.ServerAndClient");
@@ -71,7 +61,7 @@ namespace Barotrauma
}
}
public static float Range(float minimum, float maximum, RandSync sync=RandSync.Unsynced)
public static float Range(float minimum, float maximum, RandSync sync = RandSync.Unsynced)
=> GetRNG(sync).Range(minimum, maximum);
public static double Range(double minimum, double maximum, RandSync sync = RandSync.Unsynced)
@@ -1,8 +1,9 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
#if CLIENT
using Barotrauma.Networking;
@@ -259,83 +260,54 @@ namespace Barotrauma.IO
// Intentionally crash with all exceptions, if this fails.
System.IO.Directory.SetCurrentDirectory(path);
}
private static readonly EnumerationOptions IgnoreInaccessibleSystemAndHidden = new EnumerationOptions
{
MatchType = MatchType.Win32,
AttributesToSkip = FileAttributes.System | FileAttributes.Hidden,
IgnoreInaccessible = true
};
private static EnumerationOptions GetEnumerationOptions(bool ignoreInaccessible, bool recursive)
{
return new EnumerationOptions
{
MatchType = MatchType.Win32,
AttributesToSkip = FileAttributes.System | FileAttributes.Hidden,
IgnoreInaccessible = ignoreInaccessible,
RecurseSubdirectories = recursive
};
}
public static string[] GetFiles(string path)
{
try
{
return System.IO.Directory.GetFiles(path);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot get files at \"{path}\": unauthorized access. The folder/file(s) might be read-only!", e);
return Array.Empty<string>();
}
return System.IO.Directory.GetFiles(path, "*", IgnoreInaccessibleSystemAndHidden);
}
public static string[] GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories)
public static string[] GetFiles(string path, string pattern, SearchOption option = SearchOption.AllDirectories)
{
try
{
return System.IO.Directory.GetFiles(path, pattern, option);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot get files at \"{path}\": unauthorized access. The folder/file(s) might be read-only!", e);
return Array.Empty<string>();
}
EnumerationOptions enumerationOptions = GetEnumerationOptions(ignoreInaccessible: true, option == SearchOption.AllDirectories);
return System.IO.Directory.GetFiles(path, pattern, enumerationOptions);
}
public static string[] GetDirectories(string path, string searchPattern = "*", System.IO.SearchOption searchOption = System.IO.SearchOption.TopDirectoryOnly)
public static string[] GetDirectories(string path, string searchPattern = "*")
{
try
{
return System.IO.Directory.GetDirectories(path, searchPattern, searchOption);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot get directories at \"{path}\": unauthorized access. The folder(s) might be read-only!", e);
return Array.Empty<string>();
}
return System.IO.Directory.GetDirectories(path, searchPattern, IgnoreInaccessibleSystemAndHidden);
}
public static string[] GetFileSystemEntries(string path)
{
try
{
return System.IO.Directory.GetFileSystemEntries(path);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot get file system entries at \"{path}\": unauthorized access. The file/folder might be read-only!", e);
return Array.Empty<string>();
}
return System.IO.Directory.GetFileSystemEntries(path, "*", IgnoreInaccessibleSystemAndHidden);
}
public static IEnumerable<string> EnumerateDirectories(string path, string pattern)
{
try
{
return System.IO.Directory.EnumerateDirectories(path, pattern);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot enumerate directories at \"{path}\": unauthorized access. The folder(s) might be read-only!", e);
return Array.Empty<string>();
}
{
return System.IO.Directory.EnumerateDirectories(path, pattern, IgnoreInaccessibleSystemAndHidden);
}
public static IEnumerable<string> EnumerateFiles(string path, string pattern)
{
try
{
return System.IO.Directory.EnumerateFiles(path, pattern);
}
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot enumerate files at \"{path}\": unauthorized access. The file(s)/folder(s) might be read-only!", e);
return Array.Empty<string>();
}
{
return System.IO.Directory.EnumerateFiles(path, pattern, IgnoreInaccessibleSystemAndHidden);
}
public static bool Exists(string path)
@@ -343,7 +315,7 @@ namespace Barotrauma.IO
return System.IO.Directory.Exists(path);
}
public static System.IO.DirectoryInfo? CreateDirectory(string path)
public static System.IO.DirectoryInfo? CreateDirectory(string path, bool catchUnauthorizedAccessExceptions = false)
{
if (!Validation.CanWrite(path, true))
{
@@ -358,11 +330,12 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot create directory at \"{path}\": unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return null;
}
}
public static void Delete(string path, bool recursive=true)
public static void Delete(string path, bool recursive = true, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(path, true))
{
@@ -377,6 +350,7 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot delete \"{path}\": unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
@@ -384,7 +358,7 @@ namespace Barotrauma.IO
{
try
{
Delete(path, recursive);
Delete(path, recursive, catchUnauthorizedAccessExceptions: false);
return true;
}
catch
@@ -393,7 +367,7 @@ namespace Barotrauma.IO
}
}
public static DateTime GetLastWriteTime(string path)
public static DateTime GetLastWriteTime(string path, bool catchUnauthorizedAccessExceptions = true)
{
try
{
@@ -402,9 +376,57 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot get last write time at \"{path}\": unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return new DateTime();
}
}
public static void Copy(string src, string dest, bool overwrite = false)
{
if (!Validation.CanWrite(dest, true))
{
DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
return;
}
CreateDirectory(dest);
foreach (string path in GetFiles(src))
{
File.Copy(path, Path.Combine(dest, Path.GetRelativePath(src, path)), overwrite);
}
foreach (string path in GetDirectories(src))
{
Copy(path, Path.Combine(dest, Path.GetRelativePath(src, path)), overwrite);
}
}
public static void Move(string src, string dest, bool overwrite = false)
{
if (!overwrite && Exists(dest))
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": destination folder already exists.");
return;
}
if (!Validation.CanWrite(src, true))
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the source folder is not allowed.");
return;
}
if (!Validation.CanWrite(dest, true))
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": modifying the contents of the destination folder is not allowed.");
return;
}
if (!overwrite || !Exists(dest) || TryDelete(dest))
{
System.IO.Directory.Move(src, dest);
}
}
}
public static class File
@@ -413,7 +435,7 @@ namespace Barotrauma.IO
public static bool Exists(string path) => System.IO.File.Exists(path);
public static void Copy(string src, string dest, bool overwrite = false)
public static void Copy(string src, string dest, bool overwrite = false, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(dest, false))
{
@@ -427,10 +449,11 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
public static void Move(string src, string dest)
public static void Move(string src, string dest, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(src, false))
{
@@ -449,12 +472,13 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
public static void Delete(ContentPath path) => Delete(path.Value);
public static void Delete(ContentPath path, bool catchUnauthorizedAccessExceptions = true) => Delete(path.Value, catchUnauthorizedAccessExceptions);
public static void Delete(string path)
public static void Delete(string path, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(path, false))
{
@@ -468,6 +492,7 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot delete {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
@@ -480,7 +505,8 @@ namespace Barotrauma.IO
string path,
System.IO.FileMode mode,
System.IO.FileAccess access = System.IO.FileAccess.ReadWrite,
System.IO.FileShare? share = null)
System.IO.FileShare? share = null,
bool catchUnauthorizedAccessExceptions = true)
{
switch (mode)
{
@@ -508,26 +534,27 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot open {path} (stream): unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return null;
}
}
public static FileStream? OpenRead(string path)
public static FileStream? OpenRead(string path, bool catchUnauthorizedAccessExceptions = true)
{
return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read);
return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
}
public static FileStream? OpenWrite(string path)
public static FileStream? OpenWrite(string path, bool catchUnauthorizedAccessExceptions = true)
{
return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write);
return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
}
public static FileStream? Create(string path)
public static FileStream? Create(string path, bool catchUnauthorizedAccessExceptions = true)
{
return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write);
return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write, catchUnauthorizedAccessExceptions: catchUnauthorizedAccessExceptions);
}
public static void WriteAllBytes(string path, byte[] contents)
public static void WriteAllBytes(string path, byte[] contents, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(path, false))
{
@@ -541,10 +568,11 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
public static void WriteAllText(string path, string contents, System.Text.Encoding? encoding = null)
public static void WriteAllText(string path, string contents, System.Text.Encoding? encoding = null, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(path, false))
{
@@ -558,10 +586,11 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
public static void WriteAllLines(string path, IEnumerable<string> contents, System.Text.Encoding? encoding = null)
public static void WriteAllLines(string path, IEnumerable<string> contents, System.Text.Encoding? encoding = null, bool catchUnauthorizedAccessExceptions = true)
{
if (!Validation.CanWrite(path, false))
{
@@ -575,10 +604,11 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot write at {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
}
}
public static byte[] ReadAllBytes(string path)
public static byte[] ReadAllBytes(string path, bool catchUnauthorizedAccessExceptions = true)
{
try
{
@@ -587,11 +617,12 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return Array.Empty<byte>();
}
}
public static string ReadAllText(string path, System.Text.Encoding? encoding = null)
public static string ReadAllText(string path, System.Text.Encoding? encoding = null, bool catchUnauthorizedAccessExceptions = true)
{
try
{
@@ -600,11 +631,12 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return string.Empty;
}
}
public static string[] ReadAllLines(string path, System.Text.Encoding? encoding = null)
public static string[] ReadAllLines(string path, System.Text.Encoding? encoding = null, bool catchUnauthorizedAccessExceptions = true)
{
try
{
@@ -613,9 +645,20 @@ namespace Barotrauma.IO
catch (UnauthorizedAccessException e)
{
DebugConsole.ThrowError($"Cannot read {path}: unauthorized access. The file/folder might be read-only!", e);
if (!catchUnauthorizedAccessExceptions) { throw; }
return Array.Empty<string>();
}
}
public static string SanitizeName(string str)
{
string sanitized = "";
foreach (char c in str)
{
char newChar = Path.GetInvalidFileNameCharsCrossPlatform().Contains(c) ? '-' : c;
sanitized += newChar;
}
return sanitized;
}
}
public class FileStream : System.IO.Stream
@@ -11,9 +11,53 @@ using Barotrauma.IO;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.Networking;
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 +87,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
@@ -51,6 +119,20 @@ namespace Barotrauma
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
#endif
}
public static void EnsureSaveFolderExists()
{
try
{
// Create the default save folder (only) if it doesn't exist yet.
// note, uses System.IO.Directory.CreateDirectory instead of Directory.CreateDirectory from Baro namespace on purpose.
System.IO.Directory.CreateDirectory(DefaultSaveFolder);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to create the default save folder \"{DefaultSaveFolder}\"!", e);
}
}
public enum SaveType
{
@@ -58,27 +140,47 @@ 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 });
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to clear folder", e);
LogErrorAndSendToClients("Failed to clear folder", e);
return;
}
try
{
GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName));
GameMain.GameSession.Save(Path.Combine(TempPath, GameSessionFileName), isSavingOnLoading);
if (!isSavingOnLoading)
{
// Reset the campaign data path, since if we had a different load path, it would be invalid now
GameMain.GameSession.DataPath = CampaignDataPath.CreateRegular(filePath.SavePath);
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Error saving gamesession", e);
LogErrorAndSendToClients("Error saving gamesession", e);
return;
}
@@ -111,39 +213,53 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError("Error saving submarine", e);
LogErrorAndSendToClients("Error saving submarine", e);
return;
}
try
{
CompressDirectory(TempPath, filePath);
CompressDirectory(TempPath, filePath.SavePath);
}
catch (Exception e)
{
DebugConsole.ThrowError("Error compressing save file", e);
LogErrorAndSendToClients("Error compressing save file", e);
}
void LogErrorAndSendToClients(string errorMsg, Exception e)
{
DebugConsole.ThrowError(errorMsg, e);
#if SERVER
if (GameMain.Server != null)
{
foreach (var client in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(errorMsg + '\n' + e.StackTrace.CleanupStackTrace(), client, ChatMessageType.Error);
}
}
#endif
}
}
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 +301,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 +325,7 @@ namespace Barotrauma
{
try
{
File.Delete(characterDataSavePath);
File.Delete(characterDataSavePath, catchUnauthorizedAccessExceptions: false);
}
catch (Exception e)
{
@@ -229,7 +351,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 +375,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)
{
@@ -291,6 +413,7 @@ namespace Barotrauma
FilePath: file,
SaveTime: Option.None,
SubmarineName: "",
RespawnMode: RespawnMode.None,
EnabledContentPackageNames: ImmutableArray<string>.Empty));
}
else
@@ -326,6 +449,7 @@ namespace Barotrauma
FilePath: file,
SaveTime: docRoot.GetAttributeDateTime("savetime"),
SubmarineName: docRoot.GetAttributeStringUnrestricted("submarine", ""),
RespawnMode: docRoot.GetAttributeEnum("respawnmode", RespawnMode.None),
EnabledContentPackageNames: enabledContentPackageNames.ToImmutableArray()));
}
}
@@ -347,7 +471,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 +828,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
}
}
@@ -54,6 +54,13 @@ namespace Barotrauma
static partial class ToolBox
{
/// <summary>
/// Returns the Barotrauma.dll assembly.
/// Used with <see cref="ReflectionUtils.GetTypeWithBackwardsCompatibility"/>
/// </summary>
public static Assembly BarotraumaAssembly
=> Assembly.GetAssembly(typeof(GameMain));
public static bool IsProperFilenameCase(string filename)
{
//File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
@@ -68,6 +75,8 @@ namespace Barotrauma
return !corrected;
}
private static readonly Dictionary<string, string> cachedFileNames = new Dictionary<string, string>();
public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "")
{
char[] delimiters = { '/', '\\' };
@@ -82,7 +91,12 @@ namespace Barotrauma
return originalFilename;
}
#endif
if (cachedFileNames.TryGetValue(originalFilename, out string existingName))
{
// Already processed and cached.
return existingName;
}
string startPath = directory ?? "";
string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
@@ -139,6 +153,7 @@ namespace Barotrauma
if (i < subDirs.Length - 1) { filename += "/"; }
}
cachedFileNames.Add(originalFilename, filename);
return filename;
}
@@ -172,20 +187,26 @@ namespace Barotrauma
public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant());
/// <summary>
/// Convert the specified string to an integer using a deterministic formula. The same string always provides the same number, and different strings should generally provide a different number.
/// </summary>
public static int StringToInt(string str)
{
str = str.Substring(0, Math.Min(str.Length, 32));
str = str.PadLeft(4, 'a');
byte[] asciiBytes = Encoding.ASCII.GetBytes(str);
for (int i = 4; i < asciiBytes.Length; i++)
//deterministic hash function based on https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
unchecked
{
asciiBytes[i % 4] ^= asciiBytes[i];
}
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
return BitConverter.ToInt32(asciiBytes, 0);
for (int i = 0; i < str.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ str[i];
if (i == str.Length - 1) { break; }
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
/// <summary>
@@ -346,7 +367,7 @@ namespace Barotrauma
{
try
{
lines = File.ReadAllLines(filePath).ToList();
lines = File.ReadAllLines(filePath, catchUnauthorizedAccessExceptions: false).ToList();
cachedLines.Add(filePath, lines);
if (lines.Count == 0)
{
@@ -414,17 +435,23 @@ namespace Barotrauma
}
float totalWeight = weights.Sum();
float randomNum = (float)(random.NextDouble() * totalWeight);
T objectWithNonZeroWeight = default;
for (int i = 0; i < objects.Count; i++)
{
if (weights[i] > 0)
{
objectWithNonZeroWeight = objects[i];
}
if (randomNum <= weights[i])
{
return objects[i];
}
randomNum -= weights[i];
}
return default;
//it's possible for rounding errors to cause an element to not get selected if we pick a random number very close to 1
//to work around that, always return some object with a non-zero weight if none gets returned in the loop above
return objectWithNonZeroWeight;
}
/// <summary>
@@ -838,6 +865,24 @@ namespace Barotrauma
return new SquareLine(points, type);
}
/// <summary>
/// Converts a byte array to a string of hex values.
/// </summary>
/// <example>
/// { 4, 3, 75, 80 } -> "04034B50"
/// </example>
/// <param name="bytes"></param>
/// <returns></returns>
public static string BytesToHexString(byte[] bytes)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in bytes)
{
sb.Append(b.ToString("X2"));
}
return sb.ToString();
}
/// <summary>
/// Returns closest point on a rectangle to a given point.
/// If the point is inside the rectangle, the point itself is returned.
@@ -871,5 +916,26 @@ namespace Barotrauma
return closest;
}
public static ImmutableArray<uint> PrefabCollectionToUintIdentifierArray(IEnumerable<PrefabWithUintIdentifier> prefabs)
=> prefabs.Select(static p => p.UintIdentifier).ToImmutableArray();
public static ImmutableArray<T> UintIdentifierArrayToPrefabCollection<T>(PrefabCollection<T> Prefabs, IEnumerable<uint> uintIdentifiers) where T : PrefabWithUintIdentifier
{
var builder = ImmutableArray.CreateBuilder<T>();
foreach (uint uintIdentifier in uintIdentifiers)
{
var matchingPrefab = Prefabs.Find(p => p.UintIdentifier == uintIdentifier);
if (matchingPrefab == null)
{
DebugConsole.ThrowError($"Unable to find prefab with uint identifier {uintIdentifier}");
continue;
}
builder.Add(matchingPrefab);
}
return builder.ToImmutable();
}
}
}