using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Services.Processing; using Barotrauma.Steam; using FluentResults; using OneOf; // ReSharper disable UseCollectionExpression namespace Barotrauma.LuaCs.Services; public partial class PackageManagementService : IPackageManagementService { private int _isDisposed; private readonly ConcurrentDictionary _modInfos = new(); // lookup caches private readonly IPackageInfoLookupService _packageInfoLookupService; // processors private readonly IConverterServiceAsync _modConfigParserService; private readonly IProcessorService, IAssembliesResourcesInfo> _assemblyInfoConverter; private readonly IProcessorService, IConfigsResourcesInfo> _configsInfoConverter; private readonly IProcessorService, IConfigProfilesResourcesInfo> _configProfilesConverter; private readonly IProcessorService, ILuaScriptsResourcesInfo> _luaScriptsConverter; public void Dispose() { IsDisposed = true; _modInfos.Clear(); } public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } public FluentResults.Result Reset() { try { ((IService)this).CheckDisposed(); _modInfos.Clear(); } catch (Exception e) { return FluentResults.Result.Fail(new ExceptionalError(e)); } return FluentResults.Result.Ok(); } public ImmutableArray Configs => _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.SelectMany(kvp => kvp.Value.Configs).ToImmutableArray(); public ImmutableArray ConfigProfiles => _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.SelectMany(kvp => kvp.Value.ConfigProfiles).ToImmutableArray(); public ImmutableArray LuaScripts => _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.SelectMany(kvp => kvp.Value.LuaScripts).ToImmutableArray(); public ImmutableArray Assemblies => _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.SelectMany(kvp => kvp.Value.Assemblies).ToImmutableArray(); public async Task LoadPackageInfosAsync(ContentPackage package) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail(new ExceptionalError(new NullReferenceException($"{nameof(LoadPackageInfosAsync)}: ContentPackage is null."))); var result = await _modConfigParserService.TryParseResourceAsync(package); if (result.IsFailed) return FluentResults.Result.Fail($"$Could not parse package mod config.").WithErrors(result.Errors); if (!_modInfos.TryAdd(package, result.Value)) return FluentResults.Result.Fail($"Failed to add ModInfo for {package.Name}."); return FluentResults.Result.Ok(); } public async Task> LoadPackagesInfosAsync(IReadOnlyList packages) { ((IService)this).CheckDisposed(); if (packages is null || packages.Count == 0) throw new ArgumentNullException(nameof(LoadPackagesInfosAsync)); ConcurrentQueue<(ContentPackage, FluentResults.Result)> results = new(); await packages.ParallelForEachAsync(async package => { var res = await LoadPackageInfosAsync(package); results.Enqueue((package, res)); }, Environment.ProcessorCount); return results.ToImmutableArray(); } public IReadOnlyList GetAllLoadedPackages() { ((IService)this).CheckDisposed(); return _modInfos.IsEmpty ? ImmutableArray.Empty : _modInfos.Select(kvp => kvp.Key).ToImmutableArray(); } public bool IsPackageLoaded(ContentPackage package) { return package is not null && _modInfos.ContainsKey(package); } public ImmutableArray FilterUnloadableResources(IReadOnlyList resources, bool enabledPackagesOnly = false) where T : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo { return resources .Where(r => r is not null) .Where(r => (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0) .Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0) .Where(r => !r.Dependencies.Any() || r.Dependencies.All(d => d.Dependency.GetPackage() is {} p // cp is valid && _modInfos.ContainsKey(p) // cp is parsed && (!enabledPackagesOnly || _packageInfoLookupService.IsPackageEnabled(p)))) // cp is enabled .ToImmutableArray(); } public void DisposePackageInfos(ContentPackage package) { _modInfos.TryRemove(package, out _); } public void DisposePackagesInfos(IReadOnlyList packages) { if (packages is null || packages.Count == 0) return; foreach (var package in packages) { DisposePackageInfos(package); } } public Result GetPackageDependencyInfo(ContentPackage ownerPackage, string packageName, ulong steamWorkshopId) { ((IService)this).CheckDisposed(); if (ownerPackage is null) return FluentResults.Result.Fail($"OwnerPackage is null."); var nameGood = !packageName.IsNullOrWhiteSpace(); if (!nameGood && steamWorkshopId == 0) FluentResults.Result.Fail($"PackageName and SteamId cannot both be invalid."); IPackageInfo depInfo = null; // complex key if (nameGood && steamWorkshopId != 0 && _packageInfoLookupService.Lookup(packageName, steamWorkshopId).GetAwaiter().GetResult() is { IsSuccess: true, Value: {} dep1 }) { depInfo = dep1; } // name key else if (nameGood && _packageInfoLookupService.Lookup(packageName).GetAwaiter().GetResult() is { IsSuccess: true, Value: { } dep2 }) { depInfo = dep2; } // steamid key else if (_packageInfoLookupService.Lookup(steamWorkshopId).GetAwaiter().GetResult() is { IsSuccess: true, Value: { } dep3 }) { depInfo = dep3; } // this should never be null so we return an exception else { return FluentResults.Result.Fail($"Package Dependency for {ownerPackage.Name} was not found."); } return FluentResults.Result.Ok(new PackageDependency(ownerPackage, depInfo, ownerPackage.Name)); } public Result GetAssembliesInfos(ContentPackage package, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail($"{nameof(GetAssembliesInfos)}: ContentPackage is null."); if (_modInfos.TryGetValue(package, out var result)) return FluentResults.Result.Ok(_assemblyInfoConverter.Process(onlySupportedResources? result.Assemblies.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.Assemblies )); return FluentResults.Result.Fail( $"{nameof(GetAssembliesInfos)}: ContentPackage {package.Name} is not registered."); } public Result GetConfigsInfos(ContentPackage package, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail($"{nameof(GetConfigsInfos)}: ContentPackage is null."); if (_modInfos.TryGetValue(package, out var result)) { return FluentResults.Result.Ok(_configsInfoConverter.Process(onlySupportedResources? result.Configs.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.Configs )); } return FluentResults.Result.Fail( $"{nameof(GetConfigsInfos)}: ContentPackage {package.Name} is not registered."); } public Result GetConfigProfilesInfos(ContentPackage package, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail($"{nameof(GetConfigProfilesInfos)}: ContentPackage is null."); if (_modInfos.TryGetValue(package, out var result)) { return FluentResults.Result.Ok(_configProfilesConverter.Process(onlySupportedResources? result.ConfigProfiles.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.ConfigProfiles )); } return FluentResults.Result.Fail( $"{nameof(GetConfigProfilesInfos)}: ContentPackage {package.Name} is not registered."); } public Result GetLuaScriptsInfos(ContentPackage package, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (package is null) return FluentResults.Result.Fail($"{nameof(GetLuaScriptsInfos)}: ContentPackage is null."); if (_modInfos.TryGetValue(package, out var result)) { return FluentResults.Result.Ok(_luaScriptsConverter.Process(onlySupportedResources? result.LuaScripts.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.LuaScripts )); } return FluentResults.Result.Fail( $"{nameof(GetLuaScriptsInfos)}: ContentPackage {package.Name} is not registered."); } public Result GetAssembliesInfos(IReadOnlyList packages, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (packages is null || packages.Count == 0) return FluentResults.Result.Fail($"{nameof(GetAssembliesInfos)}: ContentPackage list is null or empty."); var builder = ImmutableArray.CreateBuilder(); foreach (var package in packages) { if (_modInfos.TryGetValue(package, out var result) && result.Assemblies is { IsEmpty: false }) { builder.AddRange(onlySupportedResources? result.Assemblies.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.Assemblies); } } return FluentResults.Result.Ok(_assemblyInfoConverter.Process(builder.MoveToImmutable())); } public Result GetConfigsInfos(IReadOnlyList packages, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (packages is null || packages.Count == 0) return FluentResults.Result.Fail($"{nameof(GetConfigsInfos)}: ContentPackage list is null or empty."); var builder = ImmutableArray.CreateBuilder(); foreach (var package in packages) { if (_modInfos.TryGetValue(package, out var result) && result.Configs is { IsEmpty: false }) { builder.AddRange(onlySupportedResources? result.Configs.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.Configs); } } return FluentResults.Result.Ok(_configsInfoConverter.Process(builder.MoveToImmutable())); } public Result GetConfigProfilesInfos(IReadOnlyList packages, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (packages is null || packages.Count == 0) return FluentResults.Result.Fail($"{nameof(GetConfigProfilesInfos)}: ContentPackage list is null or empty."); var builder = ImmutableArray.CreateBuilder(); foreach (var package in packages) { if (_modInfos.TryGetValue(package, out var result) && result.ConfigProfiles is { IsEmpty: false }) { builder.AddRange(onlySupportedResources? result.ConfigProfiles.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.ConfigProfiles); } } return FluentResults.Result.Ok(_configProfilesConverter.Process(builder.MoveToImmutable())); } public Result GetLuaScriptsInfos(IReadOnlyList packages, bool onlySupportedResources = true) { ((IService)this).CheckDisposed(); if (packages is null || packages.Count == 0) return FluentResults.Result.Fail($"{nameof(GetLuaScriptsInfos)}: ContentPackage list is null or empty."); var builder = ImmutableArray.CreateBuilder(); foreach (var package in packages) { if (_modInfos.TryGetValue(package, out var result) && result.LuaScripts is { IsEmpty: false }) { builder.AddRange(onlySupportedResources? result.LuaScripts.Where(r => (r.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 && (r.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0).ToImmutableArray() : result.LuaScripts); } } return FluentResults.Result.Ok(_luaScriptsConverter.Process(builder.MoveToImmutable())); } public async Task> GetAssembliesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { return await Task.Run(() => GetAssembliesInfos(packages, onlySupportedResources)); } public async Task> GetConfigsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { return await Task.Run(() => GetConfigsInfos(packages, onlySupportedResources)); } public async Task> GetConfigProfilesInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { return await Task.Run(() => GetConfigProfilesInfos(packages, onlySupportedResources)); } public async Task> GetLuaScriptsInfosAsync(IReadOnlyList packages, bool onlySupportedResources = true) { return await Task.Run(() => GetLuaScriptsInfos(packages, onlySupportedResources)); } }