Unstable 1.8.4.0
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user