Merge remote-tracking branch 'upstream/dev' into develop

This commit is contained in:
EvilFactory
2022-12-09 17:33:44 -03:00
416 changed files with 12674 additions and 5862 deletions
@@ -67,10 +67,10 @@ namespace Barotrauma
.ToImmutableHashSet();
}
public static Result<ContentFile, LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
public static Result<ContentFile, ContentPackage.LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
static Result<ContentFile, LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception));
static Result<ContentFile, ContentPackage.LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, ContentPackage.LoadError>.Failure(new ContentPackage.LoadError(error, exception));
Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
@@ -83,6 +83,8 @@ namespace Barotrauma
{
return fail($"No content path defined for file of type \"{elemName}\"");
}
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
try
{
filePath = type.MutateContentPath(filePath);
@@ -90,10 +92,16 @@ namespace Barotrauma
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found.");
}
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, LoadError>.Success(file);
if (file is null) { return fail($"Content type {type.Type.Name} is not implemented correctly"); }
if (errorCatcher.Errors.Any())
{
return fail(
$"Errors were issued to the debug console when loading \"{filePath}\" of type \"{elemName}\"");
}
return Result<ContentFile, ContentPackage.LoadError>.Success(file);
}
catch (Exception e)
{
@@ -123,23 +131,5 @@ namespace Barotrauma
}
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
public readonly struct LoadError
{
public readonly string Message;
public readonly Exception? Exception;
public LoadError(string message, Exception? exception)
{
Message = message;
Exception = exception;
}
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
}
}
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -14,11 +13,20 @@ namespace Barotrauma
{
public abstract class ContentPackage
{
public readonly record struct LoadError(string Message, Exception? Exception)
{
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
SaveUtil.SaveFolder,
SaveUtil.DefaultSaveFolder,
"WorkshopMods",
"Installed");
@@ -37,12 +45,30 @@ namespace Barotrauma
public readonly Option<DateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
/// <summary>
/// Errors that occurred when loading this content package.
/// Currently, all errors are considered fatal and the game
/// will refuse to load a content package that has any errors.
/// </summary>
public ImmutableArray<LoadError> FatalLoadErrors { get; private set; }
/// <summary>
/// An error that occurred when trying to enable this mod.
/// This field doesn't directly affect whether or not this mod
/// can be enabled, but if it's been set to anything other than
/// Option.None then the game has already refused to enable it
/// at least once.
/// </summary>
public Option<ContentPackageManager.LoadProgress.Error> EnableError { get; private set; }
= Option.None;
public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome();
public async Task<bool> IsUpToDate()
{
if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; }
if (ugcId is not SteamWorkshopId steamWorkshopId) { return true; }
if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
@@ -55,20 +81,25 @@ namespace Barotrauma
/// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary>
public bool HasMultiplayerSyncedContent { get; private set; }
public bool HasMultiplayerSyncedContent { get; }
protected ContentPackage(XDocument doc, string path)
{
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
Path = path.CleanUpPathCrossPlatform();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
Name = rootElement.GetAttributeString("name", "").Trim();
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
if (Name.IsNullOrWhiteSpace() && AltNames.Any())
{
Name = AltNames.First();
}
UgcId = steamWorkshopId != 0
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
: Option<ContentPackageId>.None();
@@ -85,23 +116,31 @@ namespace Barotrauma
.ToArray();
Files = fileResults
.OfType<Success<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Value)
.Successes()
.ToImmutableArray();
Errors = fileResults
.OfType<Failure<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Error)
FatalLoadErrors = fileResults
.Failures()
.ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), $"{nameof(Name)} is null or empty");
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
Hash = CalculateHash();
var expectedHash = rootElement.GetAttributeString("expectedhash", "");
if (HashMismatches(expectedHash))
{
DebugConsole.ThrowError($"Hash calculation for content package \"{Name}\" didn't match expected hash ({Hash.StringRepresentation} != {expectedHash})");
FatalLoadErrors = FatalLoadErrors.Add(
new LoadError(
Message: $"Hash calculation returned {Hash.StringRepresentation}, expected {expectedHash}",
Exception: null
));
}
FatalLoadErrors = FatalLoadErrors
.Concat(errorCatcher.Errors.Select(err => new LoadError(err.Text, null)))
.ToImmutableArray();
}
public bool HashMismatches(string expectedHash)
@@ -122,21 +161,21 @@ namespace Barotrauma
public bool NameMatches(string name)
=> NameMatches(name.ToIdentifier());
public static ContentPackage? TryLoad(string path)
public static Result<ContentPackage, Exception> TryLoad(string path)
{
var (success, failure) = Result<ContentPackage, Exception>.GetFactoryMethods();
XDocument doc = XMLExtensions.TryLoadXml(path);
try
{
return doc.Root.GetAttributeBool("corepackage", false)
? (ContentPackage)new CorePackage(doc, path)
: new RegularPackage(doc, path);
return success(doc.Root.GetAttributeBool("corepackage", false)
? new CorePackage(doc, path)
: new RegularPackage(doc, path));
}
catch (Exception e)
{
e = e.GetInnermost();
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
return failure(e.GetInnermost());
}
}
@@ -181,7 +220,7 @@ namespace Barotrauma
{
if (!condition)
{
throw new InvalidOperationException($"Failed to load \"{Name}\" at {Path}: {errorMsg}");
FatalLoadErrors = FatalLoadErrors.Add(new LoadError(errorMsg, null));
}
}
@@ -201,17 +240,19 @@ namespace Barotrauma
Failure
}
public LoadResult LoadPackage()
public LoadResult LoadContent()
{
foreach (var p in LoadPackageEnumerable())
foreach (var p in LoadContentEnumerable())
{
if (p.Exception != null) { return LoadResult.Failure; }
if (p.Result.IsFailure) { return LoadResult.Failure; }
}
return LoadResult.Success;
}
public IEnumerable<ContentPackageManager.LoadProgress> LoadPackageEnumerable()
public IEnumerable<ContentPackageManager.LoadProgress> LoadContentEnumerable()
{
using var errorCatcher = DebugConsole.ErrorCatcher.Create();
ContentFile[] getFilesToLoad(Predicate<ContentFile> predicate)
=> Files.Where(predicate.Invoke).ToArray()
#if DEBUG
@@ -227,6 +268,7 @@ namespace Barotrauma
for (int i = 0; i < filesToLoad.Length; i++)
{
Exception? exception = null;
try
{
//do not allow exceptions thrown here to crash the game
@@ -234,42 +276,53 @@ namespace Barotrauma
}
catch (Exception e)
{
var innermost = e.GetInnermost();
DebugConsole.LogError($"Failed to load \"{filesToLoad[i].Path}\": {innermost.Message}\n{innermost.StackTrace}");
exception = e;
}
if (exception != null)
{
yield return ContentPackageManager.LoadProgress.Failure(exception);
break;
yield break;
}
yield return new ContentPackageManager.LoadProgress((i + indexOffset) / (float)Files.Length);
if (errorCatcher.Errors.Any())
{
yield return ContentPackageManager.LoadProgress.Failure(
ContentPackageManager.LoadProgress.Error
.Reason.ConsoleErrorsThrown);
yield break;
}
yield return ContentPackageManager.LoadProgress.Progress((i + indexOffset) / (float)Files.Length);
}
}
//Load the UI and text files first. This is to allow the game
//to render the text in the loading screen as soon as possible.
var priorityFiles = getFilesToLoad(f => f is UIStyleFile || f is TextFile);
var priorityFiles = getFilesToLoad(f => f is UIStyleFile or TextFile);
var remainder = getFilesToLoad(f => !priorityFiles.Contains(f));
var loadEnumerable =
loadFiles(priorityFiles, 0)
.Concat(loadFiles(remainder, priorityFiles.Length));
foreach (var p in loadEnumerable)
{
if (p.Exception != null)
if (p.Result.TryUnwrapFailure(out var failure))
{
HandleLoadException(p.Exception);
errorCatcher.Dispose();
UnloadContent();
EnableError = Option.Some(failure);
yield return p;
break;
yield break;
}
yield return p;
}
errorCatcher.Dispose();
}
protected abstract void HandleLoadException(Exception e);
public void UnloadPackage()
public void UnloadContent()
{
Files.ForEach(f => f.UnloadFile());
}
@@ -284,21 +337,16 @@ namespace Barotrauma
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
foreach (var result in fileResults)
foreach (var file in fileResults.Successes())
{
switch (result)
if (file is BaseSubFile or ItemAssemblyFile)
{
case Success<ContentFile, ContentFile.LoadError> { Value: var file }:
if (file is BaseSubFile || file is ItemAssemblyFile)
{
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
break;
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
}
@@ -331,16 +379,16 @@ namespace Barotrauma
public void LogErrors()
{
if (!Errors.Any())
if (!FatalLoadErrors.Any())
{
return;
}
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(errorToStr)));
string.Join('\n', FatalLoadErrors.Select(errorToStr)));
static string errorToStr(ContentFile.LoadError error)
static string errorToStr(LoadError error)
=> error.ToString();
}
}
@@ -43,10 +43,5 @@ namespace Barotrauma
"Core package requires at least one of the following content types: " +
string.Join(", ", missingFileTypes.Select(t => t.Type.Name)));
}
protected override void HandleLoadException(Exception e)
{
throw new Exception($"An exception was thrown while loading \"{Name}\"", e);
}
}
}
@@ -1,4 +1,3 @@
using System;
using System.Xml.Linq;
namespace Barotrauma
@@ -9,11 +8,5 @@ namespace Barotrauma
{
AssertCondition(!doc.Root.GetAttributeBool("corepackage", false), "Expected a regular package, got a core package");
}
protected override void HandleLoadException(Exception e)
{
UnloadPackage();
DebugConsole.ThrowError($"Failed to load package \"{Name}\"", e);
}
}
}
@@ -47,18 +47,19 @@ namespace Barotrauma
{
var oldCore = Core;
if (newCore == oldCore) { yield break; }
Core?.UnloadPackage();
if (newCore.FatalLoadErrors.Any()) { yield break; }
Core?.UnloadContent();
Core = newCore;
foreach (var p in newCore.LoadPackageEnumerable()) { yield return p; }
foreach (var p in newCore.LoadContentEnumerable()) { yield return p; }
SortContent();
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void ReloadCore()
{
if (Core == null) { return; }
Core.UnloadPackage();
Core.LoadPackage();
Core.UnloadContent();
Core.LoadContent();
SortContent();
}
@@ -79,10 +80,14 @@ namespace Barotrauma
if (ReferenceEquals(inNewRegular, regular)) { yield break; }
if (inNewRegular.SequenceEqual(regular)) { yield break; }
ThrowIfDuplicates(inNewRegular);
var newRegular = inNewRegular.ToList();
var newRegular = inNewRegular
// Refuse to enable packages with load errors
// so people are forced away from broken mods
.Where(r => !r.FatalLoadErrors.Any())
.ToList();
IEnumerable<RegularPackage> toUnload = regular.Where(r => !newRegular.Contains(r));
RegularPackage[] toLoad = newRegular.Where(r => !regular.Contains(r)).ToArray();
toUnload.ForEach(r => r.UnloadPackage());
toUnload.ForEach(r => r.UnloadContent());
Range<float> loadingRange = new Range<float>(0.0f, 1.0f);
@@ -90,9 +95,9 @@ namespace Barotrauma
{
var package = toLoad[i];
loadingRange = new Range<float>(i / (float)toLoad.Length, (i + 1) / (float)toLoad.Length);
foreach (var progress in package.LoadPackageEnumerable())
foreach (var progress in package.LoadContentEnumerable())
{
if (progress.Exception != null)
if (progress.Result.IsFailure)
{
//If an exception was thrown while loading this package, refuse to add it to the list of enabled packages
newRegular.Remove(package);
@@ -103,7 +108,7 @@ namespace Barotrauma
}
regular.Clear(); regular.AddRange(newRegular);
SortContent();
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void ThrowIfDuplicates(IEnumerable<ContentPackage> pkgs)
@@ -231,10 +236,12 @@ namespace Barotrauma
public sealed partial class PackageSource : ICollection<ContentPackage>
{
private readonly Predicate<string>? skipPredicate;
private readonly Action<string, Exception>? onLoadFail;
public PackageSource(string dir, Predicate<string>? skipPredicate)
public PackageSource(string dir, Predicate<string>? skipPredicate, Action<string, Exception>? onLoadFail)
{
this.skipPredicate = skipPredicate;
this.onLoadFail = onLoadFail;
directory = dir;
Directory.CreateDirectory(directory);
}
@@ -278,25 +285,30 @@ namespace Barotrauma
{
var fileListPath = Path.Combine(subDir, ContentPackage.FileListFileName).CleanUpPathCrossPlatform();
if (this.Any(p => p.Path.Equals(fileListPath, StringComparison.OrdinalIgnoreCase))) { continue; }
if (File.Exists(fileListPath))
{
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
ContentPackage? newPackage = ContentPackage.TryLoad(fileListPath);
if (newPackage is CorePackage corePackage)
{
corePackages.Add(corePackage);
}
else if (newPackage is RegularPackage regularPackage)
{
regularPackages.Add(regularPackage);
}
if (!(newPackage is null))
{
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
if (!File.Exists(fileListPath)) { continue; }
if (skipPredicate?.Invoke(fileListPath) is true) { continue; }
var result = ContentPackage.TryLoad(fileListPath);
if (!result.TryUnwrapSuccess(out var newPackage))
{
onLoadFail?.Invoke(
fileListPath,
result.TryUnwrapFailure(out var exception) ? exception : throw new Exception("unreachable"));
continue;
}
switch (newPackage)
{
case CorePackage corePackage:
corePackages.Add(corePackage);
break;
case RegularPackage regularPackage:
regularPackages.Add(regularPackage);
break;
}
Debug.WriteLine($"Loaded \"{newPackage.Name}\"");
}
}
@@ -348,8 +360,20 @@ namespace Barotrauma
public bool IsReadOnly => true;
}
public static readonly PackageSource LocalPackages = new PackageSource(ContentPackage.LocalModsDir, skipPredicate: null);
public static readonly PackageSource WorkshopPackages = new PackageSource(ContentPackage.WorkshopModsDir, skipPredicate: SteamManager.Workshop.IsInstallingToPath);
public static readonly PackageSource LocalPackages
= new PackageSource(
ContentPackage.LocalModsDir,
skipPredicate: null,
onLoadFail: null);
public static readonly PackageSource WorkshopPackages = new PackageSource(
ContentPackage.WorkshopModsDir,
skipPredicate: SteamManager.Workshop.IsInstallingToPath,
onLoadFail: (fileListPath, exception) =>
{
// Delete Workshop mods that fail to load to
// force a reinstall on next launch if necessary
Directory.TryDelete(Path.GetDirectoryName(fileListPath)!);
});
public static CorePackage? VanillaCorePackage { get; private set; } = null;
@@ -373,63 +397,77 @@ namespace Barotrauma
EnabledPackages.DisableRemovedMods();
}
public static ContentPackage? ReloadContentPackage(ContentPackage p)
public static Result<ContentPackage, Exception> ReloadContentPackage(ContentPackage p)
{
ContentPackage? newPackage = ContentPackage.TryLoad(p.Path);
if (newPackage is CorePackage core)
{
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
}
else if (newPackage is RegularPackage regular)
{
int index = EnabledPackages.Regular.IndexOf(p);
if (index >= 0)
{
var newRegular = EnabledPackages.Regular.ToArray();
newRegular[index] = regular;
EnabledPackages.SetRegular(newRegular);
}
}
var result = ContentPackage.TryLoad(p.Path);
if (newPackage != null)
if (result.TryUnwrapSuccess(out var newPackage))
{
switch (newPackage)
{
case CorePackage core:
{
if (EnabledPackages.Core == p) { EnabledPackages.SetCore(core); }
break;
}
case RegularPackage regular:
{
int index = EnabledPackages.Regular.IndexOf(p);
if (index >= 0)
{
var newRegular = EnabledPackages.Regular.ToArray();
newRegular[index] = regular;
EnabledPackages.SetRegular(newRegular);
}
break;
}
}
LocalPackages.SwapPackage(p, newPackage);
WorkshopPackages.SwapPackage(p, newPackage);
}
EnabledPackages.DisableRemovedMods();
return newPackage;
return result;
}
public readonly struct LoadProgress
public readonly record struct LoadProgress(Result<float, LoadProgress.Error> Result)
{
public readonly float Value;
public readonly Exception? Exception;
public LoadProgress(float value)
public readonly record struct Error(
Error.Reason ErrorReason,
Option<Exception> Exception)
{
Value = value;
Exception = null;
}
public enum Reason { Exception, ConsoleErrorsThrown }
private LoadProgress(Exception exception)
{
Value = -1f;
Exception = exception;
public Error(Reason reason) : this(reason, Option.None) { }
public Error(Exception exception) : this(Reason.Exception, Option.Some(exception)) { }
}
public static LoadProgress Failure(Exception exception)
=> new LoadProgress(exception);
=> new LoadProgress(
Result<float, Error>.Failure(new Error(exception)));
public static LoadProgress Failure(Error.Reason reason)
=> new LoadProgress(
Result<float, Error>.Failure(new Error(reason)));
public static LoadProgress Progress(float value)
=> new LoadProgress(
Result<float, Error>.Success(value));
public LoadProgress Transform(Range<float> range)
=> Exception != null
? this
: new LoadProgress(MathHelper.Lerp(range.Start, range.End, Value));
=> Result.TryUnwrapSuccess(out var value)
? new LoadProgress(
Result<float, Error>.Success(
MathHelper.Lerp(range.Start, range.End, value)))
: this;
}
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
foreach (ContentFile.LoadError error in VanillaCorePackage.Errors)
foreach (ContentPackage.LoadError error in VanillaCorePackage.FatalLoadErrors)
{
DebugConsole.ThrowError(error.ToString());
}
@@ -444,6 +482,8 @@ namespace Barotrauma
if (VanillaCorePackage is null) { LoadVanillaFileList(); }
SteamManager.Workshop.DeleteUnsubscribedMods();
CorePackage enabledCorePackage = VanillaCorePackage!;
List<RegularPackage> enabledRegularPackages = new List<RegularPackage>();
@@ -512,7 +552,7 @@ namespace Barotrauma
yield return p.Transform(loadingRange);
}
yield return new LoadProgress(1.0f);
yield return LoadProgress.Progress(1.0f);
}
public static void LogEnabledRegularPackageErrors()
@@ -1,6 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Xml.Linq;
@@ -63,6 +65,8 @@ namespace Barotrauma
public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def);
public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def);
public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return:NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier>? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
@@ -126,6 +126,10 @@ namespace Barotrauma
{
return new Identifier(str);
}
internal int IndexOf(char c) => Value.IndexOf(c);
internal Identifier this[Range range] => Value[range].ToIdentifier();
internal Char this[int i] => Value[i];
}
public static class IdentifierExtensions