using System; 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; using System.Xml.Linq; using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; using FluentResults; using Microsoft.Toolkit.Diagnostics; using MoonSharp.VsCodeDebugger.SDK; namespace Barotrauma.LuaCs; public sealed class ModConfigService : IModConfigService { private IStorageService _storageService; private ILoggerService _logger; private IParserServiceAsync _assemblyParserService; private IParserServiceAsync _luaScriptParserService; private IParserServiceAsync _configParserService; #if CLIENT private IParserServiceAsync _stylesParserService; #endif private readonly AsyncReaderWriterLock _operationsLock = new(); public ModConfigService(IStorageService storageService, IParserServiceAsync assemblyParserService, IParserServiceAsync luaScriptParserService, IParserServiceAsync configParserService, #if CLIENT IParserServiceAsync stylesParserService, #endif ILoggerService logger) { _storageService = storageService; _assemblyParserService = assemblyParserService; _luaScriptParserService = luaScriptParserService; _configParserService = configParserService; _logger = logger; #if CLIENT _stylesParserService = stylesParserService; #endif } #region Dispose public void Dispose() { 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; #if CLIENT _stylesParserService.Dispose(); _stylesParserService = null; #endif } catch { // ignored } } private int _isDisposed = 0; public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } #endregion 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 }) { return await CreateFromConfigXmlAsync(src, config); } return await CreateFromLegacyAsync(src); } 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 = ImmutableArray.CreateBuilder>>>(src.Length); foreach (var srcItem in src) { builder.Add(Task.Factory.StartNew(async Task> () => await CreateConfigAsync(srcItem))); } var taskResults = await Task.WhenAll(builder.ToImmutable()); var returnResults = ImmutableArray.CreateBuilder<(ContentPackage Source, Result Config)>(); foreach (var taskResult in taskResults) { if (taskResult.IsFaulted) { ThrowHelper.ThrowInvalidOperationException($"{nameof(CreateConfigsAsync)}: Task failed: {taskResult.Exception?.Message}"); } var r = await taskResult; returnResults.Add((r.Value.Package, r)); } return returnResults.ToImmutable(); } //--- Helpers private async Task> TryGetModConfigXmlAsync(ContentPackage src) { 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 asmTask = Task.Factory.StartNew(async () => await GetAssembliesFromXml(owner, src)); var cfgTask = Task.Factory.StartNew(async () => await GetConfigsFromXml(owner, src)); var luaTask = Task.Factory.StartNew(async () => await GetLuaScriptsFromXml(owner, src)); #if CLIENT var styleTask = Task.Factory.StartNew(async () => await GetStylesFromXml(owner, src)); #endif await Task.WhenAll( asmTask, cfgTask, #if CLIENT styleTask, #endif luaTask); return FluentResults.Result.Ok(new ModConfigInfo() { Package = owner, Assemblies = await await asmTask, Configs = await await cfgTask, #if CLIENT Styles = await await styleTask, #endif LuaScripts = await await luaTask }); async Task> GetLuaScriptsFromXml(ContentPackage contentPackage, XElement cfgElement) { 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); } #if CLIENT async Task> GetStylesFromXml(ContentPackage contentPackage, XElement cfgElement) { return await GetResourceFromXml(contentPackage, cfgElement, "Style", "FileGroup", _stylesParserService); } #endif 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.ToImmutable(); } 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()); 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); foreach (var element in subLuaElems) { elems.Add(new ResourceParserInfo(package, element, cond, negCond)); } } } } return elems.ToImmutable(); } ImmutableArray GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting) { 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(); } } private async Task> CreateFromLegacyAsync(ContentPackage src) { return new ModConfigInfo() { Package = src, Assemblies = GetAssembliesLegacy(src), Configs = GetConfigsLegacy(src), LuaScripts = GetLuaScriptsLegacy(src) }; ImmutableArray GetAssembliesLegacy(ContentPackage src) { var binSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[] { ("bin/Client/Windows", Target.Client, Platform.Windows), ("bin/Client/Linux", Target.Client, Platform.Linux), ("bin/Client/OSX", Target.Client, Platform.OSX), ("bin/Server/Windows", Target.Server, Platform.Windows), ("bin/Server/Linux", Target.Server, Platform.Linux), ("bin/Server/OSX", Target.Server, Platform.OSX) }; var builder = ImmutableArray.CreateBuilder(); foreach (var searchPathways in binSearchInd) { if (_storageService.FindFilesInPackage(src, searchPathways.SubFolder, "*.dll", true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) { builder.Add(new AssemblyResourceInfo() { OwnerPackage = src, InternalName = searchPathways.SubFolder, SupportedPlatforms = searchPathways.Platforms, SupportedTargets = searchPathways.Targets, LoadPriority = 0, FilePaths = result.Value.Select(fp => ContentPath.FromRaw(src, $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) .ToImmutableArray(), FriendlyName = $"{src.Name}.{searchPathways.SubFolder.Replace('/','.')}", IncompatiblePackages = ImmutableArray.Empty, RequiredPackages = ImmutableArray.Empty, IsScript = false, IsReferenceModeOnly = false }); } } var sharedResult = _storageService.FindFilesInPackage(src, Path.Combine(src.Dir, "CSharp/Shared"), "*.cs", true); var sharedFiles = sharedResult.IsSuccess && !sharedResult.Value.IsDefaultOrEmpty ? sharedResult.Value.Select(fp => ContentPath.FromRaw(src, $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) .ToImmutableArray() : ImmutableArray.Empty; var srcSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[] { ("CSharp/Client", Target.Client, Platform.Any), ("CSharp/Server", Target.Server, Platform.Any) }; foreach (var searchPathways in srcSearchInd) { if (_storageService.FindFilesInPackage(src, searchPathways.SubFolder, "*.cs", true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) { builder.Add(new AssemblyResourceInfo() { OwnerPackage = src, InternalName = searchPathways.SubFolder, SupportedPlatforms = searchPathways.Platforms, SupportedTargets = searchPathways.Targets, LoadPriority = 0, FilePaths = result.Value .Select(fp => ContentPath.FromRaw(src, $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) .Concat(sharedFiles).ToImmutableArray(), FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, IncompatiblePackages = ImmutableArray.Empty, RequiredPackages = ImmutableArray.Empty, UseInternalAccessName = true, IsScript = true, IsReferenceModeOnly = false }); } } return builder.ToImmutable(); } ImmutableArray GetConfigsLegacy(ContentPackage src) { return ImmutableArray.Empty; } ImmutableArray GetLuaScriptsLegacy(ContentPackage src) { var builder = ImmutableArray.CreateBuilder(); if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result) { ImmutableArray cleanedResult = result.Value.Select(fp => fp.CleanUpPathCrossPlatform()).ToImmutableArray(); ImmutableArray autorun = cleanedResult .Where(fp => fp.Contains("Lua/ForcedAutorun/") || fp.Contains("Lua/Autorun/")) .ToImmutableArray(); ImmutableArray autorunFP = autorun.Select(fp => ContentPath.FromRaw(src, $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) .ToImmutableArray(); ImmutableArray reg = cleanedResult.Except(autorun) .Select(fp => ContentPath.FromRaw(src, $"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform())) .ToImmutableArray(); builder.Add(new LuaScriptsResourceInfo() { OwnerPackage = src, InternalName = "LegacyAutorun", SupportedPlatforms = Platform.Any, SupportedTargets = Target.Any, LoadPriority = 1, // autorun should be last to ensure that dependent code in other files are loaded first FilePaths = autorunFP, IncompatiblePackages = ImmutableArray.Empty, RequiredPackages = ImmutableArray.Empty, IsAutorun = true, }); builder.Add(new LuaScriptsResourceInfo() { OwnerPackage = src, InternalName = "Legacy", SupportedPlatforms = Platform.Any, SupportedTargets = Target.Any, LoadPriority = 0, // should be included first to ensure that dependent code in these files are available FilePaths = reg, IncompatiblePackages = ImmutableArray.Empty, RequiredPackages = ImmutableArray.Empty, IsAutorun = false, }); } return builder.ToImmutable(); } } }