using System; using System.Collections.Concurrent; using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs; using FluentResults; using Microsoft.Toolkit.Diagnostics; namespace Barotrauma.LuaCs; public sealed partial class ConfigService : IConfigService { #region Disposal_Locks_Reset private readonly AsyncReaderWriterLock _operationLock = new (); private readonly AsyncReaderWriterLock _settingsByPackageLock = new (); private int _isDisposed = 0; public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } public void Dispose() { using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); using var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) { return; } _logger.LogDebug($"{nameof(ConfigService)}: Disposing."); _configInfoParserService.Dispose(); _configProfileInfoParserService.Dispose(); if (!_settingsInstances.IsEmpty) { foreach (var instance in _settingsInstances) { try { if (instance.Value is null) { continue; } _eventService.PublishEvent(sub => // ReSharper disable once AccessToDisposedClosure sub.OnSettingInstanceDisposed(instance.Value)); instance.Value.Dispose(); } catch { // ignored continue; } } } _settingsInstances.Clear(); _instanceFactory.Clear(); _settingsInstancesByPackage.Clear(); _storageService = null; _logger = null; _eventService = null; _configInfoParserService = null; _configProfileInfoParserService = null; } public FluentResults.Result Reset() { using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); var result = new FluentResults.Result(); if (!_settingsInstances.IsEmpty) { foreach (var instance in _settingsInstances) { try { if (instance.Value is null) { continue; } _eventService.PublishEvent(sub => // ReSharper disable once AccessToDisposedClosure sub.OnSettingInstanceDisposed(instance.Value)); instance.Value.Dispose(); } catch (Exception e) { result.WithError(new ExceptionalError(e)); } } } _settingsInstances.Clear(); _instanceFactory.Clear(); _settingsInstancesByPackage.Clear(); _storageService.PurgeCache(); return result; } #endregion private const string SaveDataFileName = "SettingsData.xml"; // --- Settings private readonly ConcurrentDictionary<(ContentPackage OwnerPackage, string InternalName), ISettingBase> _settingsInstances = new(); private readonly ConcurrentDictionary> _instanceFactory = new(); private readonly ConcurrentDictionary> _settingsInstancesByPackage = new(); // --- Profiles private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo> _settingsProfiles = new(); private IStorageService _storageService; private ILoggerService _logger; private IEventService _eventService; private ILuaCsInfoProvider _luaCsInfoProvider; private IParserServiceOneToManyAsync _configInfoParserService; private IParserServiceOneToManyAsync _configProfileInfoParserService; public ConfigService(ILoggerService logger, IStorageService storageService, IParserServiceOneToManyAsync configInfoParserService, IParserServiceOneToManyAsync configProfileInfoParserService, IEventService eventService, ILuaCsInfoProvider luaCsInfoProvider) { _logger = logger; _storageService = storageService; _configInfoParserService = configInfoParserService; _configProfileInfoParserService = configProfileInfoParserService; _eventService = eventService; _luaCsInfoProvider = luaCsInfoProvider; _storageService.UseCaching = true; } public void RegisterSettingTypeInitializer(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) where T : class, ISettingBase { Guard.IsNotNullOrWhiteSpace(typeIdentifier, nameof(typeIdentifier)); Guard.IsNotNull(settingFactory, nameof(settingFactory)); using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (_instanceFactory.ContainsKey(typeIdentifier)) { ThrowHelper.ThrowArgumentException($"{nameof(RegisterSettingTypeInitializer)}: The type identifier {typeIdentifier} is already registered."); } _instanceFactory[typeIdentifier] = settingFactory; } public async Task LoadConfigsAsync(ImmutableArray configResources) { using var lck = await _operationLock.AcquireReaderLock(); IService.CheckDisposed(this); if (configResources.IsDefaultOrEmpty) { return FluentResults.Result.Ok(); } var taskBuilder = ImmutableArray.CreateBuilder>>(); var toProcessErrors = new ConcurrentStack(); foreach (var resource in configResources) { taskBuilder.Add(await Task.Factory.StartNew>>(async Task> () => { var r = await _configInfoParserService.TryParseResourcesAsync(resource); if (r.IsFailed) { toProcessErrors.PushRange(r.Errors.ToArray()); return ImmutableArray.Empty; } return r.Value; })); } var taskResults = await Task.WhenAll(taskBuilder.ToImmutable()); if (toProcessErrors.Count > 0) { return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Errors while loading configuration info: ").WithErrors(toProcessErrors.ToArray()); } var toProcessDocs = taskResults .Where(tr => !tr.IsDefaultOrEmpty) .SelectMany(tr => tr) .ToImmutableArray(); var instanceQueue = new Queue<(IConfigInfo configInfo, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase> factory)>(); foreach (var info in toProcessDocs) { if (!_instanceFactory.TryGetValue(info.DataType, out var factory)) { return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!"); } if (_settingsInstances.ContainsKey((info.OwnerPackage, info.InternalName))) { // duplicate for some reason (ie. double loading). This should never happen. ThrowHelper.ThrowInvalidOperationException($"{nameof(LoadConfigsAsync)}: A setting for the [ContentPackage].[InternalName] of '[{info.OwnerPackage.Name}].[{info.InternalName}]' already exists!"); } instanceQueue.Enqueue((info, factory)); } var toProcessInstanceQueue = new Queue<(IConfigInfo info, ISettingBase instance)>(); while (instanceQueue.TryDequeue(out var instanceFactoryInfo)) { try { toProcessInstanceQueue.Enqueue((instanceFactoryInfo.configInfo, instanceFactoryInfo.factory((this, instanceFactoryInfo.configInfo)))); } catch (Exception e) { FluentResults.Result.Fail( $"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!"); } } using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation var result = new FluentResults.Result(); while (toProcessInstanceQueue.TryDequeue(out var newInstanceData)) { _settingsInstances[(newInstanceData.info.OwnerPackage, newInstanceData.info.InternalName)] = newInstanceData.instance; if (!_settingsInstancesByPackage.TryGetValue(newInstanceData.info.OwnerPackage, out _)) { _settingsInstancesByPackage[newInstanceData.info.OwnerPackage] = new ConcurrentBag(); } _settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance); result.WithReasons(_eventService.PublishEvent(sub => sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons); } return result; } public async Task LoadConfigsProfilesAsync(ImmutableArray configProfileResources) { using var _ = await _operationLock.AcquireReaderLock(); IService.CheckDisposed(this); if (configProfileResources.IsDefaultOrEmpty) { ThrowHelper.ThrowArgumentNullException($"{nameof(LoadConfigsProfilesAsync)}: {nameof(configProfileResources)} is empty."); } var result = new FluentResults.Result(); foreach (var resource in configProfileResources) { var r = await _configProfileInfoParserService.TryParseResourcesAsync(resource); if (r.IsFailed) { result.WithErrors(r.Errors); continue; } foreach (var info in r.Value) { if (!_settingsProfiles.TryAdd((info.OwnerPackage, info.InternalName), info)) { result.WithErrors(r.Errors); continue; } if (info.InternalName.Equals("default", StringComparison.InvariantCultureIgnoreCase)) { //apply it foreach (var value in info.ProfileValues) { if (_settingsInstances.TryGetValue((info.OwnerPackage, value.SettingName), out var instance)) { instance.TrySetValue(value.Element); } } } } } return result; } public FluentResults.Result LoadSavedValueForConfig(ISettingBase setting) { Guard.IsNotNull(setting, nameof(setting)); using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not { } saveFileResult || saveFileResult is { IsFailed: true }) { return FluentResults.Result.Fail( $"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); } if (saveFileResult.Value.Root is not {} rootElement || !string.Equals(rootElement.Name.LocalName, "Configuration", StringComparison.InvariantCultureIgnoreCase)) { return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Root invalid for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); } if (rootElement.GetChildElement(setting.OwnerPackage.Name, StringComparison.InvariantCulture) ?.GetChildElement(setting.InternalName, StringComparison.InvariantCulture) is not {} cfgValueElement) { return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]"); } return FluentResults.Result.OkIf(setting.TrySetValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]")); } public FluentResults.Result LoadSavedConfigsValues() { ImmutableArray cfgValues; using (var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult()) { IService.CheckDisposed(this); cfgValues = _settingsInstances.Select(kvp => kvp.Value).ToImmutableArray(); } var ret = new FluentResults.Result(); foreach (var settingBase in cfgValues) { #if DEBUG // log in debug only. ret.WithReasons(LoadSavedValueForConfig(settingBase).Reasons); #else LoadSavedValueForConfig(settingBase); #endif } return ret; } public FluentResults.Result SaveConfigValue(ISettingBase setting) { XDocument cpCfgValues; if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not {} saveFileResult) { return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Storage Service Failure while trying to load file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]"); } // get Configuration if (saveFileResult.IsFailed) { cpCfgValues = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Configuration")); } else { cpCfgValues = saveFileResult.Value; } if (cpCfgValues.Root is null || cpCfgValues.Root.Name != "Configuration") { return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Bad save file format for setting: [{setting.OwnerPackage.Name}.{setting.InternalName}]"); } XElement currentTarget = GetOrAddElement(cpCfgValues.Root, setting.OwnerPackage.Name, name => new XElement(name)); currentTarget = GetOrAddElement(currentTarget, setting.InternalName, name => new XElement(name)); var ret = setting.GetSerializableValue().Match(str => { var tgt = currentTarget.Attribute("Value"); if (tgt is null) { var attr = new XAttribute("Value", str); currentTarget.Add(attr); } else { tgt.Value = str; } return FluentResults.Result.Ok(); }, elem => { currentTarget.ReplaceNodes(new XElement("Value", elem)); return FluentResults.Result.Ok(); }); ret.WithReasons(_storageService.SaveLocalXml(setting.OwnerPackage, SaveDataFileName, cpCfgValues).Reasons); return ret; XElement GetOrAddElement(XElement containerElement, string elementName, Func factory) { var element = containerElement.Element(elementName); if (element is null) { element = factory(elementName); containerElement.Add(element); } return element; } } public FluentResults.Result DisposePackageData(ContentPackage package) { Guard.IsNotNull(package, nameof(package)); using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); ConcurrentBag toDispose; using (var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult()) { if (!_settingsInstancesByPackage.TryRemove(package, out toDispose) || toDispose is null) { return FluentResults.Result.Ok(); } } var result = new FluentResults.Result(); foreach (var setting in toDispose) { result.WithReasons(_eventService.PublishEvent(sub => sub.OnSettingInstanceDisposed(setting)).Reasons); try { setting.Dispose(); } catch (Exception e) { result.WithError(new ExceptionalError(e)); } } return result; } public FluentResults.Result DisposeAllPackageData() { return this.Reset(); } public bool TryGetConfig(ContentPackage package, string internalName, out T instance) where T : ISettingBase { Guard.IsNotNull(package, nameof(package)); Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName)); using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); using var settingsLck = _settingsByPackageLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); instance = default; if(!_settingsInstances.TryGetValue((package, internalName), out var inst)) { return false; } if (inst is not T instanceT) { return false; } instance = instanceT; return true; } }