using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.LuaCs.Data; using FluentResults; namespace Barotrauma.LuaCs.Services.Processing; public partial class ModConfigService : IConverterServiceAsync, IConverterService { private readonly IStorageService _storageService; private readonly Lazy _packageManagementService; private int _isDisposed; private const string ModConfigFileName = "ModConfig.xml"; private const string ModConfigRootName = "ModConfig"; public ModConfigService(IStorageService storageService, Lazy pms) { _storageService = storageService; _packageManagementService = pms; } public void Dispose() { throw new System.NotImplementedException(); } public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } public async Task> TryParseResourceAsync(ContentPackage src) { ((IService)this).CheckDisposed(); // validate package if (src is null) return FluentResults.Result.Fail("ContentPackage is null"); if (_storageService.DirectoryExists(src.Path) is { } res && (res.IsFailed || !res.Value)) return FluentResults.Result.Fail($"ContentPackage does not exist or cannot be accessed: {src.Path}"); // find ModConfig.xml or deep scan on fail (legacy) if (await _storageService.LoadPackageXmlAsync(src, ModConfigFileName) is { IsSuccess: true, Value: var modConfigXml } && modConfigXml.Root is { Name.LocalName: ModConfigRootName } root) { return await GetModConfigInfoAsync(src, root); } // legacy mode try { // we only supported assemblies and lua scripts var asm = GetAssembliesLegacy(src); var lua = GetLuaScriptsLegacy(src); return new ModConfigInfo() { Assemblies = asm, LuaScripts = lua, Configs = ImmutableArray.Empty, ConfigProfiles = ImmutableArray.Empty, Package = src, PackageName = src.Name }; } catch (Exception e) { return FluentResults.Result.Fail($"Unable to parse legacy content package: {src.Name}: {src.Path}"); } } private partial Task> GetModConfigInfoAsync(ContentPackage package, XElement root); private ImmutableArray GetAssemblies(ContentPackage src, IEnumerable elements) { var builder = ImmutableArray.CreateBuilder(); var elementsList = elements.ToImmutableArray(); if (GetFilesList(src, elementsList, "Assembly", "*.dll") is not { IsSuccess: true, Value: { } xmlFiles }) return ImmutableArray.Empty; foreach (var file in xmlFiles) { // get platform, culture and target architecture var info = GetElementsAttributesData(file.Item1, file.Item2.First()); builder.Add(new AssemblyResourceInfo() { Optional = info.IsOptional, FilePaths = file.Item2, InternalName = info.Name, LoadPriority = info.LoadPriority, OwnerPackage = src, SupportedPlatforms = info.SupportedPlatforms, SupportedTargets = info.SupportedTargets, FriendlyName = file.Item1.GetAttributeString("Name", info.Name), IsScript = false }); } if (GetFilesList(src, elementsList, "Assembly", "*.cs") is not { IsSuccess: true, Value: { } xmlFiles2 }) return ImmutableArray.Empty; foreach (var file in xmlFiles2) { // get platform, culture and target architecture var info = GetElementsAttributesData(file.Item1, file.Item2.First()); builder.Add(new AssemblyResourceInfo() { Optional = info.IsOptional, FilePaths = file.Item2, InternalName = info.Name, LoadPriority = info.LoadPriority, OwnerPackage = src, SupportedPlatforms = info.SupportedPlatforms, SupportedTargets = info.SupportedTargets, FriendlyName = file.Item1.GetAttributeString("Name", info.Name), IsScript = true }); } return builder.Count > 0 ? builder.ToImmutable() : ImmutableArray.Empty; } private ImmutableArray GetConfigs(ContentPackage src, IEnumerable elements) { var builder = ImmutableArray.CreateBuilder(); if (GetXmlFilesList(src, elements, "Config") is not { IsSuccess: true, Value: { } xmlFiles }) return ImmutableArray.Empty; foreach (var file in xmlFiles) { // get platform, culture and target architecture var info = GetElementsAttributesData(file.Item1, file.Item2.First()); builder.Add(new ConfigResourceInfo() { Optional = info.IsOptional, FilePaths = file.Item2, InternalName = info.Name, LoadPriority = info.LoadPriority, OwnerPackage = src, SupportedPlatforms = info.SupportedPlatforms, SupportedTargets = info.SupportedTargets }); } return builder.Count > 0 ? builder.ToImmutable() : ImmutableArray.Empty; } private ImmutableArray GetConfigProfiles(ContentPackage src, IEnumerable elements) { var builder = ImmutableArray.CreateBuilder(); if (GetXmlFilesList(src, elements, "Config") is not { IsSuccess: true, Value: { } xmlFiles }) return ImmutableArray.Empty; foreach (var file in xmlFiles) { // get platform, culture and target architecture var info = GetElementsAttributesData(file.Item1, file.Item2.First()); builder.Add(new ConfigProfileResourceInfo() { Optional = info.IsOptional, FilePaths = file.Item2, InternalName = info.Name, LoadPriority = info.LoadPriority, OwnerPackage = src, SupportedPlatforms = info.SupportedPlatforms, SupportedTargets = info.SupportedTargets }); } return builder.Count > 0 ? builder.ToImmutable() : ImmutableArray.Empty; } private ImmutableArray GetLuaScripts(ContentPackage src, IEnumerable elements) { var builder = ImmutableArray.CreateBuilder(); if (GetXmlFilesList(src, elements, "Config") is not { IsSuccess: true, Value: { } xmlFiles }) return ImmutableArray.Empty; foreach (var file in xmlFiles) { // get platform, culture and target architecture var info = GetElementsAttributesData(file.Item1, file.Item2.First()); builder.Add(new LuaScriptsResourceInfo() { Optional = info.IsOptional, FilePaths = file.Item2, InternalName = info.Name, LoadPriority = info.LoadPriority, OwnerPackage = src, SupportedPlatforms = info.SupportedPlatforms, SupportedTargets = info.SupportedTargets, IsAutorun = file.Item1.GetAttributeBool("RunFile", true) }); } return builder.Count > 0 ? builder.ToImmutable() : ImmutableArray.Empty; } private Result)>> GetXmlFilesList(ContentPackage src, IEnumerable elements, string elementNameCheck) => GetFilesList(src, elements, elementNameCheck, "*.xml"); private Result)>> GetFilesList(ContentPackage src, IEnumerable elements, string elementNameCheck, string filter) { var builder = ImmutableArray.CreateBuilder<(XElement, ImmutableArray)>(); if (elementNameCheck.IsNullOrWhiteSpace()) throw new ArgumentNullException($"{nameof(GetXmlFilesList)}: The element check is null."); foreach (var element in elements) { if (element.Name.LocalName != elementNameCheck) throw new ArgumentException("Element is not a Localization element"); if (element.GetAttributeString("Folder", string.Empty) is { } str && !string.IsNullOrWhiteSpace(str)) { if (_storageService.FindFilesInPackage(src, str, filter, true) is not { IsSuccess: true, Value: var fpList } || !fpList.Any()) { continue; } foreach (var fileP in fpList) builder.Add((element, fpList.ToImmutableArray())); } else if (element.GetAttributeString("File", string.Empty) is { } fileStr && !string.IsNullOrWhiteSpace(fileStr) && _storageService.GetAbsFromPackage(src, fileStr) is { IsSuccess: true, Value: var fp } && _storageService.FileExists(fp) is { IsSuccess: true, Value: true }) { builder.Add((element, new [] { fileStr }.ToImmutableArray())); } } return builder.Count > 0 ? FluentResults.Result.Ok(builder.ToImmutable()) : FluentResults.Result.Fail($"No files found"); } private ResourceAdditionalInfo GetElementsAttributesData(XElement element, string localPath) { return new ResourceAdditionalInfo( element.GetAttributeString("Name", localPath), GetSupportedPlatforms(element.GetAttributeString("Platform", "any")), GetSupportedTargets(element.GetAttributeString("Target", "any")), GetSupportedCultures(element), element.GetAttributeBool("Optional", false), element.GetAttributeInt("Priority", 0)); Platform GetSupportedPlatforms(string platformName) => platformName.ToLowerInvariant().Trim() switch { "windows" => Platform.Windows, "linux" => Platform.Linux, "osx" => Platform.OSX, _ => Platform.Windows | Platform.Linux | Platform.OSX }; Target GetSupportedTargets(string targetName) => targetName.ToLowerInvariant().Trim() switch { "client" => Target.Client, "server" => Target.Server, _ => Target.Client | Target.Server, }; ImmutableArray GetSupportedCultures(XElement element) { var culture = element.GetAttributeString("Culture", string.Empty); if (string.IsNullOrWhiteSpace(culture)) return new[] { CultureInfo.InvariantCulture }.ToImmutableArray(); var builder = ImmutableArray.CreateBuilder(); var arr = culture.Split(','); if (arr.Length == 0) return new[] { CultureInfo.InvariantCulture }.ToImmutableArray(); foreach (var culstr in arr) { if (string.IsNullOrWhiteSpace(culstr)) continue; try { builder.Add( culstr.ToLowerInvariant().Trim() == "default" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culstr)); } catch (CultureNotFoundException e) { // This is the case if a culture is specified by the package that is not supported by the OS/.NET ENV. // We ignore it since we can never use it. continue; } } return builder.Count > 0 ? builder.ToImmutable() : new[] { CultureInfo.InvariantCulture }.ToImmutableArray(); } } private ImmutableArray GetAssembliesLegacy(ContentPackage src) { var builder = ImmutableArray.CreateBuilder(); // server, linux if (_storageService.FindFilesInPackage(src, "bin/Server/Linux", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesSrvLin}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesSrvLin, FriendlyName = "AssembliesServerLinux", InternalName = "AssembliesServerLinux", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Linux, SupportedTargets = Target.Server }); } // server, osx if (_storageService.FindFilesInPackage(src, "bin/Server/OSX", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesSrvOsx}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesSrvOsx, FriendlyName = "AssembliesServerOSX", InternalName = "AssembliesServerOSX", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.OSX, SupportedTargets = Target.Server }); } // server, osx if (_storageService.FindFilesInPackage(src, "bin/Server/Windows", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesSrvWin}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesSrvWin, FriendlyName = "AssembliesServerWin", InternalName = "AssembliesServerWin", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Windows, SupportedTargets = Target.Server }); } // client, linux if (_storageService.FindFilesInPackage(src, "bin/Client/Linux", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesCliLin}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesCliLin, FriendlyName = "AssembliesClientLinux", InternalName = "AssembliesClientLinux", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Linux, SupportedTargets = Target.Client }); } // server, osx if (_storageService.FindFilesInPackage(src, "bin/Client/OSX", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesCliOsx}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesCliOsx, FriendlyName = "AssembliesClientOSX", InternalName = "AssembliesClientOSX", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.OSX, SupportedTargets = Target.Client }); } // server, osx if (_storageService.FindFilesInPackage(src, "bin/Client/Windows", "*.dll", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesCliWin}) { builder.Add(new AssemblyResourceInfo() { FilePaths = filesCliWin, FriendlyName = "AssembliesClientWin", InternalName = "AssembliesClientWin", IsScript = false, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Windows, SupportedTargets = Target.Client }); } var sharedCsBuilder = ImmutableArray.CreateBuilder(); if (_storageService.FindFilesInPackage(src, "CSharp/Shared", "*.cs", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } files }) { sharedCsBuilder.AddRange(files); } var filesCssShared = sharedCsBuilder.MoveToImmutable(); var sharedFound = !filesCssShared.IsDefaultOrEmpty; // source files legacy: server if (_storageService.FindFilesInPackage(src, "CSharp/Server", "*.cs", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesCssServer}) { builder.Add(new AssemblyResourceInfo() { FilePaths = sharedFound ? filesCssServer.Concat(filesCssShared).ToImmutableArray() : filesCssServer, FriendlyName = "CssServer", InternalName = "CssServer", IsScript = true, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Linux | Platform.OSX | Platform.Windows, SupportedTargets = Target.Server }); } // source files legacy: client if (_storageService.FindFilesInPackage(src, "CSharp/Client", "*.cs", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false} filesCssClient}) { builder.Add(new AssemblyResourceInfo() { FilePaths = sharedFound ? filesCssClient.Concat(filesCssShared).ToImmutableArray() : filesCssClient, FriendlyName = "CssClient", InternalName = "CssClient", IsScript = true, LoadPriority = 1, Optional = false, OwnerPackage = src, SupportedPlatforms = Platform.Linux | Platform.OSX | Platform.Windows, SupportedTargets = Target.Client }); } return builder.MoveToImmutable(); } private ImmutableArray GetLuaScriptsLegacy(ContentPackage src) { var builder = ImmutableArray.CreateBuilder(); if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true) is { IsSuccess: true, Value: { IsDefaultOrEmpty: false } fileAll }) { builder.Add(new LuaScriptsResourceInfo() { FilePaths = fileAll.Where(path => !path.Contains("Autorun")).ToImmutableArray(), InternalName = "LuaScriptsNormal", Optional = false, IsAutorun = false, OwnerPackage = src, SupportedPlatforms = Platform.Linux | Platform.OSX | Platform.Windows, SupportedTargets = Target.Client | Target.Server }); builder.Add(new LuaScriptsResourceInfo() { FilePaths = fileAll.Where(path => path.Contains("Autorun")).ToImmutableArray(), InternalName = "LuaScriptsAutorun", Optional = false, IsAutorun = true, OwnerPackage = src, SupportedPlatforms = Platform.Linux | Platform.OSX | Platform.Windows, SupportedTargets = Target.Client | Target.Server }); } return builder.MoveToImmutable(); } public async Task>> TryParseResourcesAsync(IEnumerable sources) { ((IService)this).CheckDisposed(); var srcs = sources.ToImmutableArray(); var results = new AsyncLocal>>(); await srcs.ParallelForEachAsync(async pkg => { try { results.Value.Enqueue(await TryParseResourceAsync(pkg)); } catch (Exception e) { // this should never happen but this is to stop partial execution exit. results.Value.Enqueue( FluentResults.Result.Fail($"Failed to parse package {pkg?.Name}: {e.Message}")); } }); return results.Value.ToImmutableArray(); } public Result TryParseResource(ContentPackage src) => TryParseResourceAsync(src).GetAwaiter().GetResult(); public ImmutableArray> TryParseResources(IEnumerable sources) => TryParseResourcesAsync(sources.ToImmutableArray()).GetAwaiter().GetResult(); private record ResourceAdditionalInfo( string Name, Platform SupportedPlatforms, Target SupportedTargets, ImmutableArray SupportedCultures, bool IsOptional, int LoadPriority); }