From 3e81e2716007e94bceeedbcbd1628c3e02d6f4a2 Mon Sep 17 00:00:00 2001 From: MapleWheels Date: Thu, 8 Jan 2026 11:35:34 -0500 Subject: [PATCH] [Save/Sync] In-Progress ModConfigXml loading rewrite. + Fixed async operations lock for Dispose() pattern in working files. + Rewrote StorageService.cs: --- Now uses ContentPath instead of raw strings where possible. --- Now throws exceptions for developer errors and critical program states. + Rewrote ModConfigService.cs: --- All functions are now completely async. + Removed ConfigProfilesResources completely as they exist in common Config xml files. + Somewhat simplified package data and processes. --- .../Data/DataInterfaceImplementations.cs | 7 +- .../SharedSource/LuaCs/Data/IModConfigInfo.cs | 8 +- .../LuaCs/Data/IResourceInfoDeclarations.cs | 7 - .../LuaCs/Data/ServicesConfigData.cs | 2 +- .../SharedSource/LuaCs/LuaCsSetup.cs | 13 - .../LuaCs/Services/ConfigService.cs | 6 +- .../Services/PackageManagementService.cs | 16 - .../Processing/ConfigFileParserService.cs | 225 ++++++++ .../Services/Processing/ModConfigService.cs | 168 ++++-- .../LuaCs/Services/StorageService.cs | 502 +++++++++--------- .../Services/_Interfaces/IConfigService.cs | 2 +- ...IModConfigInfo.cs => IModConfigService.cs} | 0 .../_Interfaces/IPackageManagementService.cs | 5 +- .../LuaCs/Services/_Interfaces/IService.cs | 9 +- .../Services/_Interfaces/IStorageService.cs | 28 +- 15 files changed, 651 insertions(+), 347 deletions(-) create mode 100644 Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigFileParserService.cs rename Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/{IModConfigInfo.cs => IModConfigService.cs} (100%) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs index 26fb2464a..3c2c84007 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceImplementations.cs @@ -20,7 +20,6 @@ public record ModConfigInfo : IModConfigInfo public ImmutableArray Assemblies { get; init; } public ImmutableArray LuaScripts { get; init; } public ImmutableArray Configs { get; init; } - public ImmutableArray ConfigProfiles { get; init; } } #endregion @@ -30,7 +29,6 @@ public record ModConfigInfo : IModConfigInfo public record AssemblyResourcesInfo(ImmutableArray Assemblies) : IAssembliesResourcesInfo; public record LuaScriptsResourcesInfo(ImmutableArray LuaScripts) : ILuaScriptsResourcesInfo; public record ConfigResourcesInfo(ImmutableArray Configs) : IConfigsResourcesInfo; -public record ConfigProfilesResourcesInfo(ImmutableArray ConfigProfiles) : IConfigProfilesResourcesInfo; public record BaseResourceInfo : IBaseResourceInfo { @@ -51,10 +49,11 @@ public record AssemblyResourceInfo : BaseResourceInfo, IAssemblyResourceInfo public bool IsScript { get; init; } } +/// +/// Note: Config settings and settings-profiles are stored in the same files. +/// public record ConfigResourceInfo : BaseResourceInfo, IConfigResourceInfo {} -public record ConfigProfileResourceInfo : BaseResourceInfo, IConfigProfileResourceInfo {} - public record LuaScriptsResourceInfo : BaseResourceInfo, ILuaScriptResourceInfo { public bool IsAutorun { get; init; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs index 77acdc7df..c8740e51d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IModConfigInfo.cs @@ -1,18 +1,18 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Xml.Linq; namespace Barotrauma.LuaCs.Data; public partial interface IModConfigInfo : IAssembliesResourcesInfo, - ILuaScriptsResourcesInfo, IConfigsResourcesInfo, - IConfigProfilesResourcesInfo + ILuaScriptsResourcesInfo, IConfigsResourcesInfo { // package info ContentPackage Package { get; } } public record ResourceParserInfo( - ContentPackage Owner, - XElement Element, + [NotNull] ContentPackage Owner, + [NotNull] XElement Element, ImmutableArray Required, ImmutableArray Incompatible); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs index 4dd4910f7..366578fdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -9,8 +9,6 @@ public interface IBaseResourceInfo : IResourceInfo, IDataInfo, IDependencyInfo { public interface IConfigResourceInfo : IBaseResourceInfo {} -public interface IConfigProfileResourceInfo :IBaseResourceInfo {} - /// /// Represents loadable Lua files. /// @@ -53,9 +51,4 @@ public interface IConfigsResourcesInfo ImmutableArray Configs { get; } } -public interface IConfigProfilesResourcesInfo -{ - ImmutableArray ConfigProfiles { get; } -} - #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs index b5ee25eb9..cce3a273b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs @@ -112,7 +112,7 @@ public record StorageServiceConfig : IStorageServiceConfig, IStorageServiceConfi public string LocalDataSavePath => Path.Combine(ExecutionLocation, "/Data/Mods/"); - public string LocalDataPathRegex => ""; + public string LocalDataPathRegex => "%ModDir%"; public string RunLocation => ExecutionLocation; public bool GlobalSafeIOEnabled => false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 4714df7cc..95d7a92af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -544,7 +544,6 @@ namespace Barotrauma async Task LoadStaticAssetsAsync(IReadOnlyList packages) { var cfgRes = ImmutableArray.Empty; - var cfpRes = ImmutableArray.Empty; var luaRes = ImmutableArray.Empty; var tasksBuilder = ImmutableArray.CreateBuilder(); @@ -561,15 +560,6 @@ namespace Barotrauma res.ToResult()); })(), new Func(async () => - { - var res = await PackageManagementService.GetConfigProfilesInfosAsync(packages); - if (res.IsSuccess) - cfpRes = res.Value.ConfigProfiles; - if (res.Errors.Any()) - ThreadPool.QueueUserWorkItem(state => Logger.LogResults((FluentResults.Result)state), - res.ToResult()); - })(), - new Func(async () => { var res = await PackageManagementService.GetLuaScriptsInfosAsync(packages); if (res.IsSuccess) @@ -588,9 +578,6 @@ namespace Barotrauma var res = await ConfigService.LoadConfigsAsync(cfgRes); if (res.Errors.Any()) Logger.LogResults(res); - res = await ConfigService.LoadConfigsProfilesAsync(cfpRes); - if (res.Errors.Any()) - Logger.LogResults(res); })(), new Func(async () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs index 47648cab3..df6e6c39f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ConfigService.cs @@ -24,7 +24,7 @@ namespace Barotrauma.LuaCs.Services; public partial class ConfigService : IConfigService { //--- Internals - public ConfigService(IParserServiceAsync> configProfileResourceParser, + public ConfigService(IParserServiceAsync> configProfileResourceParser, IParserServiceAsync> configResourceParser, IEventService eventService, System.Lazy storageService) { _configProfileResourceParser = configProfileResourceParser; @@ -48,7 +48,7 @@ public partial class ConfigService : IConfigService // extern services private readonly IParserServiceAsync> _configResourceParser; - private readonly IParserServiceAsync> _configProfileResourceParser; + private readonly IParserServiceAsync> _configProfileResourceParser; private readonly IEventService _eventService; private readonly System.Lazy _storageService; @@ -357,7 +357,7 @@ public partial class ConfigService : IConfigService return ret; } - public async Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources) + public async Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources) { using var lck = await _disposeOpsLock.AcquireReaderLock(); _base.CheckDisposed(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs index 064813b9e..a14ed5784 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs @@ -27,7 +27,6 @@ public partial class PackageManagementService : IPackageManagementService } public ImmutableArray Configs { get; } - public ImmutableArray ConfigProfiles { get; } public ImmutableArray LuaScripts { get; } public ImmutableArray Assemblies { get; } public async Task LoadPackageInfosAsync(ContentPackage package) @@ -75,11 +74,6 @@ public partial class PackageManagementService : IPackageManagementService throw new System.NotImplementedException(); } - public Result GetConfigProfilesInfos(ContentPackage package, bool onlySupportedResources = true) - { - throw new System.NotImplementedException(); - } - public Result GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true) { throw new System.NotImplementedException(); @@ -95,11 +89,6 @@ public partial class PackageManagementService : IPackageManagementService throw new System.NotImplementedException(); } - public Result GetConfigProfilesInfos(IReadOnlyList packages, bool onlySupportedResources = true) - { - throw new System.NotImplementedException(); - } - public Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true) { throw new System.NotImplementedException(); @@ -115,11 +104,6 @@ public partial class PackageManagementService : IPackageManagementService throw new System.NotImplementedException(); } - public async Task> GetConfigProfilesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) - { - throw new System.NotImplementedException(); - } - public async Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { throw new System.NotImplementedException(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigFileParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigFileParserService.cs new file mode 100644 index 000000000..292ac3eea --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ConfigFileParserService.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Xml.Linq; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.Toolkit.Diagnostics; + +namespace Barotrauma.LuaCs.Services.Processing; + +public sealed class ConfigFileParserService : + IParserServiceAsync, + IParserServiceAsync, + IParserServiceAsync +{ + private IStorageService _storageService; + private readonly AsyncReaderWriterLock _operationsLock = new(); + + public ConfigFileParserService(IStorageService storageService) + { + _storageService = storageService; + } + + #region Dispose + + public void Dispose() + { + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + try + { + _storageService.Dispose(); + this._storageService = null; + } + catch + { + // ignored + } + } + + private int _isDisposed = 0; + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + + #endregion + + // --- Assemblies + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireWriterLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Assembly") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await GetCheckedFiles(src.Element, src.Owner, ".dll"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new AssemblyResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible, + // Type Specific + FriendlyName = src.Element.GetAttributeString("FriendlyName", string.Empty), + IsScript = src.Element.GetAttributeBool("IsScript", false), + }; + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Config + + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + + using var lck = await _operationsLock.AcquireWriterLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Config") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await GetCheckedFiles(src.Element, src.Owner, ".xml"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new ConfigResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible + }; + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Lua Scripts + async Task> IParserServiceAsync.TryParseResourceAsync(ResourceParserInfo src) + { + using var lck = await _operationsLock.AcquireWriterLock(); + IService.CheckDisposed(this); + + if (CheckThrowNullRefs(src, "Lua") is { IsFailed: true } fail) + return fail; + + var runtimeEnv = GetRuntimeEnvironment(src.Element); + var fileResults = await GetCheckedFiles(src.Element, src.Owner, ".lua"); + + if (fileResults.IsFailed) + return FluentResults.Result.Fail(fileResults.Errors); + + return new LuaScriptsResourceInfo() + { + SupportedPlatforms = runtimeEnv.Platform, + SupportedTargets = runtimeEnv.Target, + LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0), + FilePaths = fileResults.Value, + Optional = src.Element.GetAttributeBool("Optional", false), + InternalName = src.Element.GetAttributeString("Name", string.Empty), + OwnerPackage = src.Owner, + RequiredPackages = src.Required, + IncompatiblePackages = src.Incompatible, + // Type Specific + IsAutorun = src.Element.GetAttributeBool("RunFile", false) + }; + } + + private FluentResults.Result CheckThrowNullRefs(ResourceParserInfo src, string elementName) + { + Guard.IsNotNull(src, nameof(src)); + Guard.IsNotNull(src.Owner, nameof(src.Owner)); + Guard.IsNotNull(src.Element, nameof(src.Element)); + + if (src.Element.Name != elementName) + { + return FluentResults.Result.Fail($"Element name '{elementName}' is incorrect"); + } + + return FluentResults.Result.Ok(); + } + + async Task>> IParserServiceAsync.TryParseResourcesAsync(IEnumerable sources) + { + return await this.TryParseGenericResourcesAsync(sources); + } + + // --- Helpers + private async Task>> GetCheckedFiles(XElement srcElement, ContentPackage srcOwner, string fileExtension) + { + using var lck = await _operationsLock.AcquireWriterLock(); + IService.CheckDisposed(this); + + var builder = ImmutableArray.CreateBuilder(); + var filePath = srcElement.GetAttributeString("File", string.Empty); + var folderPath = srcElement.GetAttributeString("Folder", string.Empty); + + if (!filePath.IsNullOrWhiteSpace()) + { + var cp = ContentPath.FromRaw(srcOwner, filePath); + if (_storageService.FileExists(cp.FullPath) is { IsSuccess: true, Value: true }) + { + builder.Add(cp); + } + } + + if (!folderPath.IsNullOrWhiteSpace()) + { + var cp = ContentPath.FromRaw(srcOwner, folderPath); + if (_storageService.DirectoryExists(cp.FullPath) is { IsSuccess: true, Value: true }) + { + var files = _storageService.FindFilesInPackage(cp.ContentPackage, cp.Value, fileExtension, true); + } + } + + throw new NotImplementedException(); + } + private (Platform Platform, Target Target) GetRuntimeEnvironment(XElement element) + { + return ( + Platform: element.GetAttributeEnum("Platform", Platform.Windows | Platform.Linux | Platform.OSX), + Target: element.GetAttributeEnum("Target", Target.Client | Target.Server)); + } + + private async Task>> TryParseGenericResourcesAsync(IEnumerable sources) + { + // ReSharper disable once PossibleMultipleEnumeration + Guard.IsNotNull(sources, nameof(IParserServiceAsync.TryParseResourcesAsync)); + var builder = ImmutableArray.CreateBuilder>(); + foreach (var info in sources) + { + builder.Add(await Unsafe.As>(this).TryParseResourceAsync(info)); + } + return builder.ToImmutable(); + } + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs index 2274692ac..d9d783a93 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/ModConfigService.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,29 +18,51 @@ namespace Barotrauma.LuaCs.Services.Processing; public sealed class ModConfigService : IModConfigService { private IStorageService _storageService; + private ILoggerService _logger; private IParserServiceAsync _assemblyParserService; private IParserServiceAsync _luaScriptParserService; private IParserServiceAsync _configParserService; - private IParserServiceAsync _configProfileParserService; + private readonly AsyncReaderWriterLock _operationsLock = new(); public ModConfigService(IStorageService storageService, IParserServiceAsync assemblyParserService, IParserServiceAsync luaScriptParserService, IParserServiceAsync configParserService, - IParserServiceAsync configProfileParserService) + ILoggerService logger) { _storageService = storageService; _assemblyParserService = assemblyParserService; _luaScriptParserService = luaScriptParserService; _configParserService = configParserService; - _configProfileParserService = configProfileParserService; + _logger = logger; } #region Dispose public void Dispose() { - throw new NotImplementedException(); + using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + + try + { + _storageService.Dispose(); + _logger.Dispose(); + _assemblyParserService.Dispose(); + _luaScriptParserService.Dispose(); + _configParserService.Dispose(); + + _storageService = null; + _logger = null; + _assemblyParserService = null; + _luaScriptParserService = null; + _configParserService = null; + } + catch + { + // ignored + } } private int _isDisposed = 0; @@ -54,6 +77,8 @@ public sealed class ModConfigService : IModConfigService public async Task> CreateConfigAsync(ContentPackage src) { Guard.IsNotNull(src, nameof(src)); + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config }) { @@ -65,6 +90,11 @@ public sealed class ModConfigService : IModConfigService public async Task Config)>> CreateConfigsAsync(ImmutableArray src) { + if (src.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException($"{nameof(CreateConfigsAsync)}: The supplied array is default or empty!"); + using var lck = await _operationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + var builder = new ConcurrentQueue<(ContentPackage Source, Result Config)>(); await src.ParallelForEachAsync(async package => @@ -79,55 +109,125 @@ public sealed class ModConfigService : IModConfigService //--- Helpers private async Task> TryGetModConfigXmlAsync(ContentPackage src) { - return await _storageService.LoadPackageXmlAsync(src, "ModConfig.xml") is { IsSuccess: true, Value: { Root: {} config} } + return await _storageService.LoadPackageXmlAsync(ContentPath.FromRaw(src, "%ModDir%/ModConfig.xml")) is { IsSuccess: true, Value: { Root: {} config} } ? FluentResults.Result.Ok(config) : FluentResults.Result.Fail("ModConfig.xml not found"); } private async Task> CreateFromConfigXmlAsync(ContentPackage owner, XElement src) { - /*var cfg = src.GetChildElements("Config"); - var modConfig = new ModConfigInfo() + ImmutableArray assemblyResources = default; + ImmutableArray configResources = default; + ImmutableArray luaResources = default; + + var res = await Task.WhenAll(new[] + { + new Task(async () => assemblyResources = await GetAssembliesFromXml(owner, src)), + new Task(async () => configResources = await GetConfigsFromXml(owner, src)), + new Task(async () => luaResources = await GetLuaScriptsFromXml(owner, src)), + }); + + bool isFaulted = false; + foreach (var task in res) + { + if (task.IsFaulted) + { + _logger.LogError($"{nameof(CreateFromConfigXmlAsync)}: {task.Exception?.ToString()}"); + isFaulted = true; + } + } + + if (isFaulted) + { + _logger.LogError($"{nameof(CreateFromConfigXmlAsync)}: Failed to process content package: {owner.Name}"); + return FluentResults.Result.Fail($"{nameof(CreateFromConfigXmlAsync)}: Failed to process content package: {owner.Name}"); + } + + return FluentResults.Result.Ok(new ModConfigInfo() { Package = owner, - Assemblies = src.GetChildElements("Assembly") is {} asm ? GetAssembliesFromXml(owner, asm) - : ImmutableArray.Empty, - Configs = cfg is {} ? GetConfigsFromXml(owner, cfg) : ImmutableArray.Empty, - ConfigProfiles = cfg is {} ? GetConfigProfilesFromXml(owner, cfg) : ImmutableArray.Empty, - LuaScripts = src.GetChildElements("Lua") is {} lua ? GetLuaScriptsFromXml(owner, lua) - : ImmutableArray.Empty - };*/ + Assemblies = assemblyResources, + Configs = configResources, + LuaScripts = luaResources + }); - async Task>> GetLuaScriptsFromXml(ContentPackage contentPackage, + async Task> GetLuaScriptsFromXml(ContentPackage contentPackage, XElement cfgElement) { - var luaElems = cfgElement.GetChildElements("Lua").ToImmutableArray(); - if (cfgElement.GetChildElements("FileGroup").ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroup - && fileGroup.SelectMany(fg => fg.GetChildElements())) + return await GetResourceFromXml(contentPackage, cfgElement, "Lua", "FileGroup", _luaScriptParserService); + } + + async Task> GetConfigsFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Config", "FileGroup", _configParserService); + + } + + async Task> GetAssembliesFromXml(ContentPackage contentPackage, + XElement cfgElement) + { + return await GetResourceFromXml(contentPackage, cfgElement, "Assembly", "FileGroup", _assemblyParserService); + } + + async Task> GetResourceFromXml(ContentPackage contentPackage, XElement cfgElement, string elemName, string fileGroupName, IParserServiceAsync resourceService) + { + var elems = GetResourceElementsWithName(owner, cfgElement, elemName, fileGroupName); + if (elems.IsDefaultOrEmpty) + return ImmutableArray.Empty; + + var results = await resourceService.TryParseResourcesAsync(elems); + Guard.IsNotEmpty((IReadOnlyCollection>)results, nameof(results)); + + var resources = ImmutableArray.CreateBuilder(); + foreach (var result in results) { - + if (result.Errors.Count > 0) + { + _logger.LogResults(result.ToResult()); + continue; + } + resources.Add(result.Value); } + return resources.MoveToImmutable(); + } + + ImmutableArray GetResourceElementsWithName(ContentPackage package, XElement root, string elemName, string groupName) + { + var elems = ImmutableArray.CreateBuilder(); + elems.AddRange(root.GetChildElements(elemName) + .Select(e => new ResourceParserInfo(package, e, ImmutableArray.Empty, ImmutableArray.Empty)) + .ToImmutableArray()); - throw new NotImplementedException(); - } + if (root.GetChildElements(groupName).ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroups) + { + foreach (var fileGroup in fileGroups) + { + if (fileGroup.GetChildElements(elemName).ToImmutableArray() is { IsDefaultOrEmpty: false } subLuaElems) + { + var cond = GetDependencyIdentifiers(fileGroup, true); + var negCond = GetDependencyIdentifiers(fileGroup, false); - async Task>> GetConfigProfilesFromXml(ContentPackage contentPackage, - XElement cfgElement) - { - throw new NotImplementedException(); - } + foreach (var element in subLuaElems) + { + elems.Add(new ResourceParserInfo(package, element, cond, negCond)); + } + } + } + } - async Task>> GetConfigsFromXml(ContentPackage contentPackage, - XElement cfgElement) - { - throw new NotImplementedException(); + return elems.MoveToImmutable(); } - - async Task>> GetAssembliesFromXml(ContentPackage contentPackage, - XElement cfgElement) + + ImmutableArray GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting) { - throw new NotImplementedException(); + return fg.GetChildElements("Conditional") + .Where(cElem => bool.TryParse(cElem.GetAttribute("IsLoaded").Value, out bool isLoaded) && isLoaded == depsLoadedSetting) + .SelectMany(cElem2 => cElem2.GetAttributeString("Dependencies", String.Empty) + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .Select(ident => new Identifier(ident))) + .ToImmutableArray(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs index d1650b962..724b2c4dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/StorageService.cs @@ -2,52 +2,52 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; -using System.Collections.ObjectModel; using System.IO; -using System.Linq; -using System.Reflection; -using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.LuaCs.Data; -using Barotrauma.Steam; using FluentResults; using FluentResults.LuaCs; using Microsoft.Toolkit.Diagnostics; using Error = FluentResults.Error; -using Path = Barotrauma.IO.Path; +using Path = System.IO.Path; namespace Barotrauma.LuaCs.Services; public class StorageService : IStorageService { - public StorageService(IStorageServiceConfig configData) { - _configData = configData; + ConfigData = configData; } private readonly ConcurrentDictionary> _fsCache = new(); - protected readonly IStorageServiceConfig _configData; - + protected readonly IStorageServiceConfig ConfigData; + protected readonly AsyncReaderWriterLock OperationsLock = new(); + public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); private int _isDisposed = 0; public void Dispose() { - ModUtils.Threading.SetBool(ref _isDisposed, true); + if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) + return; + using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); + _fsCache.Clear(); } public void PurgeCache() { - ((IService)this).CheckDisposed(); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); _fsCache.Clear(); } public void PurgeFileFromCache(string absolutePath) { - ((IService)this).CheckDisposed(); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); if (absolutePath.IsNullOrWhiteSpace()) return; @@ -67,7 +67,8 @@ public class StorageService : IStorageService public void PurgeFilesFromCache(params string[] absolutePaths) { - ((IService)this).CheckDisposed(); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); if (absolutePaths.Length < 1) return; @@ -91,6 +92,201 @@ public class StorageService : IStorageService } } + // --- Local Game Content + protected Result GetAbsolutePathForLocal(ContentPackage package, string localFilePath) + { + if (Path.IsPathRooted(localFilePath)) + ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path."); + + try + { + var path = System.IO.Path.GetFullPath(Path.Combine( + ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, package.ToIdentifier().Value) + .CleanUpPathCrossPlatform(), + localFilePath)); + if (!path.StartsWith(ConfigData.LocalDataSavePath)) + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!"); + return path; + } + catch (Exception e) + { + if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException) + throw; // these are dev errors and should be propagated. + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + + private Result LoadLocalData(ContentPackage package, string localFilePath, Func> dataLoader) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value); + } + + public Result LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml); + public Result LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary); + public Result LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText); + + + private FluentResults.Result SaveLocalData(ContentPackage package, string localFilePath, in T data, Func dataSaver) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data); + } + + public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) + => SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data)); + public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) + => SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data)); + public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) + => SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data)); + + private async Task> LoadLocalDataAsync(ContentPackage package, string localFilePath, + Func>> dataLoader) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value); + } + + public async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path)); + public async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path)); + public async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) + => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path)); + + private async Task SaveLocalDataAsync(ContentPackage package, string localFilePath, + T data, Func> dataSaver) + { + Guard.IsNotNull(package, nameof(package)); + Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + IService.CheckDisposed(this); + using var lck = await OperationsLock.AcquireReaderLock(); + var res = GetAbsolutePathForLocal(package, localFilePath); + return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data); + } + + public async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) + => await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc)); + public async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) + => await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin)); + public async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) + => await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt)); + + + // --- Package Content + private Result LoadPackageData(ContentPath filePath, Func> dataLoader) + { + Guard.IsNotNull(filePath, nameof(filePath)); + Guard.IsNotNullOrWhiteSpace(filePath.FullPath, nameof(filePath.FullPath)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (!filePath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory) && !filePath.FullPath.StartsWith(ConfigData.LocalModsDirectory)) + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{filePath.FullPath}' is not in a package directory!"); + return dataLoader(filePath.FullPath); + } + + public Result LoadPackageXml(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath)); + public Result LoadPackageBinary(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath)); + public Result LoadPackageText(ContentPath filePath) + => LoadPackageData(filePath, path => TryLoadText(filePath.FullPath)); + + private ImmutableArray<(ContentPath, Result)> LoadPackageDataFiles(ImmutableArray filePaths, Func> dataLoader) + { + if (filePaths.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); + foreach (var path in filePaths) + { + builder.Add((path, LoadPackageData(path, dataLoader))); + } + return builder.MoveToImmutable(); + } + + public ImmutableArray<(ContentPath, Result)> LoadPackageXmlFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadXml); + public ImmutableArray<(ContentPath, Result)> LoadPackageBinaryFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadBinary); + public ImmutableArray<(ContentPath, Result)> LoadPackageTextFiles(ImmutableArray filePaths) + => LoadPackageDataFiles(filePaths, TryLoadText); + + public Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) + { + Guard.IsNotNull(package, nameof(package)); + try + { + var fullPath = localSubfolder.IsNullOrWhiteSpace() + ? Path.GetFullPath(package.Path) + : Path.GetFullPath(package.Path, localSubfolder); + return System.IO.Directory.GetFiles(fullPath, regexFilter, + searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) + .ToImmutableArray(); + } + catch (Exception e) + { + if (e is ArgumentNullException or ArgumentException) + throw; + return FluentResults.Result.Fail(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + } + + + private async Task> LoadPackageDataAsync(ContentPath filePath, Func>> dataLoader) + { + Guard.IsNotNull(filePath, nameof(filePath)); + Guard.IsNotNullOrWhiteSpace(filePath.FullPath, nameof(filePath.FullPath)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + if (!filePath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory) && !filePath.FullPath.StartsWith(ConfigData.LocalModsDirectory)) + ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{filePath.FullPath}' is not in a package directory!"); + return await dataLoader(filePath.FullPath); + } + + public async Task> LoadPackageXmlAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path)); + public async Task> LoadPackageBinaryAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path)); + public async Task> LoadPackageTextAsync(ContentPath filePath) + => await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path)); + + private async Task)>> LoadPackageDataFilesAsync( + ImmutableArray filePaths, Func>> dataLoader) + { + if (filePaths.IsDefaultOrEmpty) + ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); + using var lck = await OperationsLock.AcquireReaderLock(); + var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); + foreach (var path in filePaths) + { + builder.Add((path, await LoadPackageDataAsync(path, dataLoader))); + } + return builder.MoveToImmutable(); + } + + public async Task)>> LoadPackageXmlFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path)); + public async Task)>> LoadPackageBinaryFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path)); + public async Task)>> LoadPackageTextFilesAsync(ImmutableArray filePaths) + => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path)); + + private int _useCaching; public bool UseCaching { @@ -98,156 +294,14 @@ public class StorageService : IStorageService set => ModUtils.Threading.SetBool(ref _useCaching, value); } - public virtual FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadXml(r.Value) : r.ToResult(); - public virtual FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadBinary(r.Value) : r.ToResult(); - public virtual FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadText(r.Value) : r.ToResult(); - public virtual FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TrySaveXml(r.Value, document) : r.ToResult(); - public virtual FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TrySaveBinary(r.Value, bytes) : r.ToResult(); - public virtual FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TrySaveText(r.Value, text) : r.ToResult(); - public virtual async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadXmlAsync(r.Value) : r.ToResult(); - public virtual async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); - public virtual async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadTextAsync(r.Value) : r.ToResult(); - public virtual async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TrySaveXmlAsync(r.Value, document) : r.ToResult(); - public virtual async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TrySaveBinaryAsync(r.Value, bytes) : r.ToResult(); - public virtual async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) => - GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TrySaveTextAsync(r.Value, text) : r.ToResult(); - public virtual FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadXml(r.Value) : r.ToResult(); - public virtual FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadBinary(r.Value) : r.ToResult(); - public virtual FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? TryLoadText(r.Value) : r.ToResult(); + // Method group redirect + private FluentResults.Result TryLoadXml(string filePath) => TryLoadXml(filePath, null); - - - public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePaths) + public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding) { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) - builder.Add((path, LoadPackageXml(package, path))); - return builder.MoveToImmutable(); - } - - public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePaths) - { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) - builder.Add((path, LoadPackageBinary(package, path))); - return builder.MoveToImmutable(); - } - - public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePaths) - { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - foreach (var path in localFilePaths) - builder.Add((path, LoadPackageText(package, path))); - return builder.MoveToImmutable(); - } - - public virtual FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) - { - ((IService)this).CheckDisposed(); - var r = GetAbsoluePathFromPackage(package, localSubfolder); - if (r is { IsFailed: true }) - return r.ToResult(); - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result>)>(); - var sOption = searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; - string[] arr = Directory.GetFiles(localSubfolder, regexFilter.IsNullOrWhiteSpace() ? "*.*" : regexFilter, sOption); - return new FluentResults.Result>().WithSuccess($"Files found.") - .WithValue(arr.ToImmutableArray()); - } - - public virtual async Task> LoadPackageXmlAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadXmlAsync(r.Value) : r.ToResult(); - - public virtual async Task> LoadPackageBinaryAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); - - public virtual async Task> LoadPackageTextAsync(ContentPackage package, string localFilePath) => - GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } - ? await TryLoadTextAsync(r.Value) : r.ToResult(); - - public virtual async Task)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray localFilePaths) - { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); - await localFilePaths.ParallelForEachAsync(async path => - { - builder.Add((path, await LoadPackageXmlAsync(package, path))); - }, maxDegreeOfParallelism: 2); - return builder.MoveToImmutable(); - } - - public virtual async Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths) - { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - await localFilePaths.ParallelForEachAsync(async path => - { - builder.Add((path, await LoadPackageBinaryAsync(package, path))); - }, maxDegreeOfParallelism: 2); - return builder.MoveToImmutable(); - } - - public virtual async Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths) - { - ((IService)this).CheckDisposed(); - if (localFilePaths.IsDefaultOrEmpty) - return ImmutableArray<(string, FluentResults.Result)>.Empty; - var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); - await localFilePaths.ParallelForEachAsync(async path => - { - builder.Add((path, await LoadPackageTextAsync(package, path))); - }, maxDegreeOfParallelism: 2); - return builder.MoveToImmutable(); - } - - - public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null) - { - ((IService)this).CheckDisposed(); var r = TryLoadText(filePath, encoding); if (r is { IsSuccess: true, Value: not null }) return XDocument.Parse(r.Value); @@ -258,9 +312,13 @@ public class StorageService : IStorageService } } - public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding = null) + // Method group redirect + private FluentResults.Result TryLoadText(string filePath) => TryLoadText(filePath, null); + public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding) { - ((IService)this).CheckDisposed(); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) && result.TryPickT1(out var cachedVal, out _)) { @@ -280,7 +338,9 @@ public class StorageService : IStorageService public virtual FluentResults.Result TryLoadBinary(string filePath) { - ((IService)this).CheckDisposed(); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + if (UseCaching && _fsCache.TryGetValue(filePath, out var result) && result.TryPickT0(out var cachedVal, out _)) { @@ -301,14 +361,10 @@ public class StorageService : IStorageService public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding); public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null) { - ((IService)this).CheckDisposed(); - if (text.IsNullOrWhiteSpace()) - { - return FluentResults.Result.Fail($"Contents are empty for {filePath}") - .WithError(new Error($"Contents are empty for {filePath}") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.Sources, filePath)); - } + Guard.IsNotNullOrWhiteSpace(text, nameof(text)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + string t = text; //copy return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () => { @@ -321,16 +377,15 @@ public class StorageService : IStorageService }); } + public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) { - ((IService)this).CheckDisposed(); - if (bytes is null || bytes.Length == 0) - { - return FluentResults.Result.Fail($"Byte array is null or empty for {filePath}") - .WithError(new Error($"Byte array is null or empty for {filePath}") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.Sources, filePath)); - } + Guard.IsNotNull(bytes, nameof(bytes)); + Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); + using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); + IService.CheckDisposed(this); + + byte[] b = new byte[bytes.Length]; System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () => @@ -346,7 +401,7 @@ public class StorageService : IStorageService public virtual FluentResults.Result FileExists(string filePath) { - ((IService)this).CheckDisposed(); + IService.CheckDisposed(this); return IOExceptionsOperationRunner(nameof(FileExists), filePath, () => { var fp = filePath.CleanUpPath(); @@ -357,7 +412,7 @@ public class StorageService : IStorageService public virtual FluentResults.Result DirectoryExists(string directoryPath) { - ((IService)this).CheckDisposed(); + IService.CheckDisposed(this); try { var di = new DirectoryInfo(directoryPath); @@ -371,7 +426,7 @@ public class StorageService : IStorageService public virtual async Task> TryLoadXmlAsync(string filePath, Encoding encoding = null) { - ((IService)this).CheckDisposed(); + IService.CheckDisposed(this); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT2(out var cachedDoc, out _)) return FluentResults.Result.Ok(cachedDoc); @@ -391,7 +446,7 @@ public class StorageService : IStorageService public virtual async Task> TryLoadTextAsync(string filePath, Encoding encoding = null) { - ((IService)this).CheckDisposed(); + IService.CheckDisposed(this); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT1(out var cachedTxt, out _)) return FluentResults.Result.Ok(cachedTxt); @@ -409,7 +464,7 @@ public class StorageService : IStorageService public virtual async Task> TryLoadBinaryAsync(string filePath) { - ((IService)this).CheckDisposed(); + IService.CheckDisposed(this); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT0(out var cachedBin, out _)) { @@ -427,14 +482,9 @@ public class StorageService : IStorageService public virtual async Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding); public virtual async Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null) { - ((IService)this).CheckDisposed(); - if (text.IsNullOrWhiteSpace()) - { - return FluentResults.Result.Fail($"Contents are empty for {filePath}") - .WithError(new Error($"Contents are empty for {filePath}") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.Sources, filePath)); - } + Guard.IsNotNullOrWhiteSpace(text, nameof(text)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); string t = text.ToString(); //copy return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () => @@ -450,14 +500,12 @@ public class StorageService : IStorageService public virtual async Task TrySaveBinaryAsync(string filePath, byte[] bytes) { - ((IService)this).CheckDisposed(); - if (bytes is null || bytes.Length == 0) - { - return FluentResults.Result.Fail($"Byte array is null or empty for {filePath}") - .WithError(new Error($"Byte array is null or empty for {filePath}") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.Sources, filePath)); - } + Guard.IsNotNull(bytes, nameof(bytes)); + Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); + using var lck = await OperationsLock.AcquireReaderLock(); + IService.CheckDisposed(this); + + byte[] b = new byte[bytes.Length]; System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () => @@ -479,6 +527,8 @@ public class StorageService : IStorageService } catch (Exception e) { + if (e is ArgumentException or ArgumentNullException) + throw; return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -491,6 +541,8 @@ public class StorageService : IStorageService } catch (Exception e) { + if (e is ArgumentException or ArgumentNullException) + throw; return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -503,6 +555,8 @@ public class StorageService : IStorageService } catch (Exception e) { + if (e is ArgumentException or ArgumentNullException) + throw; return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -515,6 +569,8 @@ public class StorageService : IStorageService } catch (Exception e) { + if (e is ArgumentException or ArgumentNullException) + throw; return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } @@ -530,50 +586,6 @@ public class StorageService : IStorageService .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, localfp); - private FluentResults.Result GetAbsoluePathFromLocal(ContentPackage package, string localFilePath) - { - if (Path.IsPathRooted(localFilePath)) - { - return new FluentResults.Result().WithError( - new Error($"The path '{localFilePath}' is a rooted path. Must be relative!") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, localFilePath)); - } - - Guard.IsNotNull(package, nameof(package)); - - return new FluentResults.Result().WithSuccess($"Path constructed") - .WithValue(System.IO.Path.GetFullPath(System.IO.Path.Combine( - _configData.RunLocation, - _configData.LocalPackageDataPath.Replace( - _configData.LocalDataPathRegex, - package.TryExtractSteamWorkshopId(out var id) ? id.Value.ToString() : package.Name), - localFilePath))); - } - - public FluentResults.Result GetAbsoluePathFromPackage(ContentPackage package, string localFilePath) - { - Guard.IsNotNull(package, nameof(package)); - - if (localFilePath.IsNullOrWhiteSpace()) - { - return new FluentResults.Result().WithValue(Path.GetFullPath(package.Path.CleanUpPath())); - } - - var path = localFilePath.CleanUpPath(); - - if (Path.IsPathRooted(path)) - { - return new FluentResults.Result().WithError( - new Error($"The path '{localFilePath}' is a rooted path. Must be relative!") - .WithMetadata(MetadataType.ExceptionObject, this) - .WithMetadata(MetadataType.RootObject, localFilePath)); - } - - return new FluentResults.Result().WithSuccess($"Path constructed") - .WithValue(Path.Combine(Path.GetFullPath(package.Path.CleanUpPath()), path)); - } - private FluentResults.Result ReturnException(TException exception, ContentPackage package) where TException : Exception { return new FluentResults.Result().WithError(new ExceptionalError(exception) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs index b26cc724b..f9f08d689 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs @@ -27,7 +27,7 @@ public partial interface IConfigService : IReusableService, ILuaConfigService // Config Files/Resources Task LoadConfigsAsync(ImmutableArray configResources); - Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources); + Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources); // Immediate Mode FluentResults.Result AddConfig(IConfigInfo configInfo) where TConfig : IConfigBase; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IModConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IModConfigService.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IModConfigInfo.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IModConfigService.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs index 0f185aa8b..ae566c125 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs @@ -9,7 +9,7 @@ using Barotrauma.LuaCs.Data; namespace Barotrauma.LuaCs.Services; -public interface IPackageManagementService : IReusableService, IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo, IAssembliesResourcesInfo +public interface IPackageManagementService : IReusableService, IConfigsResourcesInfo, ILuaScriptsResourcesInfo, IAssembliesResourcesInfo { /// /// Loads and parses the provided for supported by the current runtime environment. @@ -46,16 +46,13 @@ public interface IPackageManagementService : IReusableService, IConfigsResources // single FluentResults.Result GetAssembliesInfos(ContentPackage package, bool onlySupportedResources = true); FluentResults.Result GetConfigsInfos(ContentPackage package, bool onlySupportedResources = true); - FluentResults.Result GetConfigProfilesInfos(ContentPackage package, bool onlySupportedResources = true); FluentResults.Result GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true); // collection FluentResults.Result GetAssembliesInfos(IReadOnlyList packages, bool onlySupportedResources = true); FluentResults.Result GetConfigsInfos(IReadOnlyList packages, bool onlySupportedResources = true); - FluentResults.Result GetConfigProfilesInfos(IReadOnlyList packages, bool onlySupportedResources = true); FluentResults.Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetAssembliesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetConfigsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); - Task> GetConfigProfilesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs index 3e8457e19..a5502fc94 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Toolkit.Diagnostics; namespace Barotrauma.LuaCs.Services; @@ -25,6 +26,12 @@ public interface IService : IDisposable public void CheckDisposed() { if (IsDisposed) - throw new ObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!"); + ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!"); + } + + static void CheckDisposed(IService service) + { + if (service.IsDisposed) + ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{service.GetType().Name}'!"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs index 58ca07d55..19f112840 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using FluentResults; namespace Barotrauma.LuaCs.Services; @@ -28,7 +29,7 @@ public interface IStorageService : IService /// void PurgeFilesFromCache(params string[] absolutePaths); - // -- local game folder storage + // -- local game folder storage FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath); FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath); FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath); @@ -45,24 +46,23 @@ public interface IStorageService : IService // -- package directory // singles - FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath); - FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath); - FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath); + Result LoadPackageXml(ContentPath filePath); + Result LoadPackageBinary(ContentPath filePath); + Result LoadPackageText(ContentPath filePath); // collections - ImmutableArray<(string, FluentResults.Result)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePaths); - ImmutableArray<(string, FluentResults.Result)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePaths); - ImmutableArray<(string, FluentResults.Result)> LoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePaths); + ImmutableArray<(ContentPath, Result)> LoadPackageXmlFiles(ImmutableArray filePaths); + ImmutableArray<(ContentPath, Result)> LoadPackageBinaryFiles(ImmutableArray filePaths); + ImmutableArray<(ContentPath, Result)> LoadPackageTextFiles(ImmutableArray filePaths); FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively); - FluentResults.Result GetAbsoluePathFromPackage(ContentPackage package, string localFilePath); // async // singles - Task> LoadPackageXmlAsync(ContentPackage package, string localFilePath); - Task> LoadPackageBinaryAsync(ContentPackage package, string localFilePath); - Task> LoadPackageTextAsync(ContentPackage package, string localFilePath); + Task> LoadPackageXmlAsync(ContentPath filePath); + Task> LoadPackageBinaryAsync(ContentPath filePath); + Task> LoadPackageTextAsync(ContentPath filePath); // collections - Task)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray localFilePaths); - Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths); - Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths); + Task)>> LoadPackageXmlFilesAsync(ImmutableArray filePaths); + Task)>> LoadPackageBinaryFilesAsync(ImmutableArray filePaths); + Task)>> LoadPackageTextFilesAsync(ImmutableArray filePaths); // -- absolute paths FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null);