* Update bug-reports.yml * Fix modifyChatMessage hook * Add LuaCsSetup.Lua back for compatibility * Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table * Actually use the PackageId const everywhere we need to refer to our content package * Load languages files even if the package is disabled * Fix Hook.Remove not being implemented properly * - Changed event aliases to be case insensitive. * - Fixed assembly logging style. - Fixed double logging during execution. * Fix garbage network data being read by the game when reading LuaCs network messages * PackageId -> PackageName * Added caching toggle to PluginManagementService * Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server * Oops, fix NRE crash * Fix hide username in logs config not doing anything * Fix Cs prompt showing up more than one between rounds * Fix server host being prompted twice with the C# popup * Ignore our workshop packages from the game's dependency thing since it doesn't really make sense * Load console commands after executing and possible fix for the not console command permitted * Added fallback friendly name resolution for ModConfig assembly contents. * Register Voronoi2 stuff * Added configinfo null check to SettingBase.cs * Add safety check so this stops crashing when we look at it the wrong way * Fixed "Folder" attribute files not being found. * Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be) * Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails. * Added legacy overload of AddCommand for mod compat. * Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API. * Changed csharp script compilation algorithm to be best effort. * Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox. * - Fixed networking sync vars failing to sync initially. - Fixed lua failing to differentiate overloads ISettingBase. * Add alias for human.CPRSuccess and human.CPRFailed * - Fixed up the settings menu. - Made SettingEntry throw an error if "Value" attribute is not found in XML. - Fixed saved values for settings sometimes not reloading after disabling and re-enabling a package. * Fix LuaCs net messages received during connection initialization to be read incorrectly, happened because we would reset the BitPosition in our harmony patch which would cause the message to be read incorrectly later * Allow reloadlua to force the state to running * New icon for settings and make the top left text more user friendly * Fix client.packages hook sending normal packages * Fixed OnUpdate() not passing in deltaTime instead of totalTime. * Missing diffs frombb21a09244* Added networking tests for configs. * Added missing diffs forf61f852a25. * Some tweaks to the text * Remove missing Value error, it should just use the default value if it's not specified * Fix UseInternalAccessName * Always purge cashes for plugin content on unloading. * Fix texture not multiple of 4 * v1.12.7.0 (Spring Update 2026 Hotfix 1) --------- Co-authored-by: Joonas Rikkonen <poe.regalis@gmail.com> Co-authored-by: Evil Factory <36804725+evilfactory@users.noreply.github.com> Co-authored-by: MapleWheels <njainanan@hotmail.com>
683 lines
26 KiB
C#
683 lines
26 KiB
C#
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;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.LuaCs.Data;
|
|
using Barotrauma.LuaCs.Events;
|
|
using Barotrauma.LuaCs;
|
|
using FluentResults;
|
|
using Microsoft.Toolkit.Diagnostics;
|
|
using Microsoft.Xna.Framework;
|
|
|
|
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<IEventSettingInstanceLifetime>(sub =>
|
|
// ReSharper disable once AccessToDisposedClosure
|
|
sub.OnSettingInstanceDisposed(instance.Value));
|
|
instance.Value.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
_settingsInstances.Clear();
|
|
_instanceFactory.Clear();
|
|
_settingsInstancesByPackage.Clear();
|
|
_commandsService.Dispose();
|
|
|
|
_storageService = null;
|
|
_logger = null;
|
|
_eventService = null;
|
|
_configInfoParserService = null;
|
|
_configProfileInfoParserService = null;
|
|
_commandsService = null;
|
|
_infoProvider = 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<IEventSettingInstanceLifetime>(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<string, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase>>
|
|
_instanceFactory = new();
|
|
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<ISettingBase>>
|
|
_settingsInstancesByPackage = new();
|
|
|
|
// --- Profiles
|
|
private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo>
|
|
_settingsProfiles = new();
|
|
|
|
private IStorageService _storageService;
|
|
private ILoggerService _logger;
|
|
private IEventService _eventService;
|
|
private IConsoleCommandsService _commandsService;
|
|
private ILuaCsInfoProvider _infoProvider;
|
|
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> _configInfoParserService;
|
|
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> _configProfileInfoParserService;
|
|
|
|
public ConfigService(ILoggerService logger,
|
|
IStorageService storageService,
|
|
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> configInfoParserService,
|
|
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> configProfileInfoParserService,
|
|
IEventService eventService,
|
|
IConsoleCommandsService commandsService,
|
|
ILuaCsInfoProvider infoProvider)
|
|
{
|
|
_logger = logger;
|
|
_storageService = storageService;
|
|
_configInfoParserService = configInfoParserService;
|
|
_configProfileInfoParserService = configProfileInfoParserService;
|
|
_eventService = eventService;
|
|
_commandsService = commandsService;
|
|
_infoProvider = infoProvider;
|
|
|
|
_storageService.UseCaching = false;
|
|
InjectCommands(commandsService);
|
|
}
|
|
|
|
private void InjectCommands(IConsoleCommandsService commandsService)
|
|
{
|
|
commandsService.RegisterCommand("cfg_getvalue", "cfg_getvalue [Content Package] [InternalName] [ValueString]: gets a config value.", (string[] args) =>
|
|
{
|
|
if (args.Length < 1)
|
|
{
|
|
_logger.LogError("Please specify the name of the package to set the config.");
|
|
return;
|
|
}
|
|
|
|
if (args.Length < 2)
|
|
{
|
|
_logger.LogError("Please specify the name of the config.");
|
|
return;
|
|
}
|
|
|
|
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
|
|
if (package == null)
|
|
{
|
|
_logger.LogError($"Could not find the package {args[0]}!");
|
|
return;
|
|
}
|
|
|
|
string internalName = args[1];
|
|
|
|
if (!TryGetConfig(package, internalName, out ISettingBase setting))
|
|
{
|
|
_logger.LogError($"Could not get config with name {internalName}");
|
|
return;
|
|
}
|
|
|
|
_logger.LogMessage($"config {internalName} value is {setting.GetStringValue()}", Color.Green);
|
|
}, getValidArgs: () => new[]
|
|
{
|
|
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
|
});
|
|
|
|
commandsService.RegisterCommand("cfg_setvalue", "cfg_setvalue [Content Package] [InternalName] [ValueString]: sets a config.", (string[] args) =>
|
|
{
|
|
if (args.Length < 1)
|
|
{
|
|
_logger.LogError("Please specify the name of the package to set the config.");
|
|
return;
|
|
}
|
|
|
|
if (args.Length < 2)
|
|
{
|
|
_logger.LogError("Please specify the name of the config.");
|
|
return;
|
|
}
|
|
|
|
if (args.Length < 3)
|
|
{
|
|
_logger.LogError("Please specify the value to set the config to.");
|
|
return;
|
|
}
|
|
|
|
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
|
|
if (package == null)
|
|
{
|
|
_logger.LogError($"Could not find the package {args[0]}!");
|
|
return;
|
|
}
|
|
|
|
string internalName = args[1];
|
|
string valueString = args[2];
|
|
|
|
if (!TryGetConfig(package, internalName, out ISettingBase setting))
|
|
{
|
|
_logger.LogError($"Could not get config with name {internalName}");
|
|
return;
|
|
}
|
|
|
|
if (setting.TrySetSerializedValue(valueString))
|
|
{
|
|
_logger.LogMessage($"Set config {internalName} value to {valueString}", Color.Green);
|
|
if (SaveConfigValue(setting) is { IsFailed: true } res)
|
|
{
|
|
_logger.LogMessage($"Failed to save new config data to disk. Reasons: {res.ToString()}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.LogError($"Failed to set config value");
|
|
}
|
|
}, getValidArgs: () => new[]
|
|
{
|
|
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
|
});
|
|
|
|
commandsService.RegisterCommand("cfg_setprofile", "cfg_setprofile [ContentPackage] [InternalProfileName]",
|
|
(string[] args) =>
|
|
{
|
|
if (args.Length < 1 || args[0].IsNullOrWhiteSpace())
|
|
{
|
|
_logger.LogError("Please specify the name of the package of the profile.");
|
|
return;
|
|
}
|
|
|
|
if (args.Length < 2 || args[1].IsNullOrWhiteSpace())
|
|
{
|
|
_logger.LogError("Please specify the name of the profile.");
|
|
return;
|
|
}
|
|
|
|
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0], null);
|
|
if (package == null)
|
|
{
|
|
_logger.LogError($"Could not find the package {args[0]}!");
|
|
return;
|
|
}
|
|
|
|
var res = ApplyConfigProfile(package, args[1]);
|
|
if (res.IsFailed)
|
|
{
|
|
_logger.LogError($"Errors while applying profile {args[1]}!");
|
|
_logger.LogResults(res);
|
|
return;
|
|
}
|
|
_logger.Log($"Profile {args[1]} applied successfully!", Color.Green);
|
|
}, getValidArgs: () => new[]
|
|
{
|
|
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
|
}, false);
|
|
}
|
|
|
|
public void RegisterSettingTypeInitializer<T>(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;
|
|
}
|
|
|
|
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources) where T : IBaseResourceInfo
|
|
{
|
|
return resources
|
|
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
|
|
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
|
|
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
|
|
.ThenBy(r => r.LoadPriority)
|
|
.ToImmutableArray();
|
|
}
|
|
|
|
public async Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources)
|
|
{
|
|
using var lck = await _operationLock.AcquireReaderLock();
|
|
IService.CheckDisposed(this);
|
|
if (configResources.IsDefaultOrEmpty)
|
|
{
|
|
return FluentResults.Result.Ok();
|
|
}
|
|
|
|
var result = new FluentResults.Result();
|
|
|
|
var taskBuilder = ImmutableArray.CreateBuilder<Task<ImmutableArray<IConfigInfo>>>();
|
|
var toProcessErrors = new ConcurrentStack<IError>();
|
|
|
|
foreach (var resource in SelectCompatible(configResources))
|
|
{
|
|
taskBuilder.Add(await Task.Factory.StartNew<Task<ImmutableArray<IConfigInfo>>>(async Task<ImmutableArray<IConfigInfo>> () =>
|
|
{
|
|
var r = await _configInfoParserService.TryParseResourcesAsync(resource);
|
|
if (r.IsFailed)
|
|
{
|
|
toProcessErrors.PushRange(r.Errors.ToArray());
|
|
return ImmutableArray<IConfigInfo>.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)
|
|
.Where(icf => icf is not null)
|
|
.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))
|
|
{
|
|
result.WithError(
|
|
$"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!");
|
|
continue;
|
|
}
|
|
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)
|
|
{
|
|
result.WithError(
|
|
$"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation
|
|
|
|
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<ISettingBase>();
|
|
}
|
|
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance);
|
|
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
|
|
sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> 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 SelectCompatible(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.TrySetSerializedValue(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)
|
|
{
|
|
#if DEBUG
|
|
return FluentResults.Result.Fail(
|
|
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
|
#endif
|
|
return FluentResults.Result.Ok();
|
|
}
|
|
|
|
if (saveFileResult is { IsFailed: true })
|
|
{
|
|
#if DEBUG
|
|
_logger.LogResults(saveFileResult.ToResult());
|
|
return FluentResults.Result.Fail(
|
|
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
|
#endif
|
|
return FluentResults.Result.Ok();
|
|
}
|
|
|
|
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(XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), StringComparison.InvariantCultureIgnoreCase)
|
|
?.GetChildElement(setting.InternalName, StringComparison.InvariantCultureIgnoreCase) is not {} cfgValueElement)
|
|
{
|
|
#if DEBUG
|
|
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
|
#endif
|
|
return FluentResults.Result.Ok();
|
|
}
|
|
|
|
return FluentResults.Result.OkIf(setting.TrySetSerializedValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]"));
|
|
}
|
|
|
|
public FluentResults.Result LoadSavedConfigsValues()
|
|
{
|
|
ImmutableArray<ISettingBase> 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 ApplyConfigProfile(ContentPackage package, string internalName)
|
|
{
|
|
Guard.IsNotNull(package, nameof(package));
|
|
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
|
|
using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
IService.CheckDisposed(this);
|
|
|
|
if (!_settingsProfiles.TryGetValue((package, internalName), out var setting))
|
|
{
|
|
return FluentResults.Result.Fail($"{nameof(ApplyConfigProfile)}: Could not find profile [{package.Name}.{internalName}]");
|
|
}
|
|
|
|
var result = new FluentResults.Result();
|
|
|
|
foreach (var profileValue in setting.ProfileValues)
|
|
{
|
|
if (!_settingsInstances.TryGetValue((package, profileValue.SettingName), out var instance))
|
|
{
|
|
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Could not find setting [{profileValue.SettingName}]."));
|
|
continue;
|
|
}
|
|
|
|
if (!instance.TrySetSerializedValue(profileValue.Element))
|
|
{
|
|
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Failed to set value for [{profileValue.SettingName}]."));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
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, XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), 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<string, XElement> 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<ISettingBase> 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<IEventSettingInstanceLifetime>(sub => sub.OnSettingInstanceDisposed(setting)).Reasons);
|
|
try
|
|
{
|
|
_settingsInstances.TryRemove((setting.OwnerPackage, setting.InternalName), out _);
|
|
setting.Dispose();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
result.WithError(new ExceptionalError(e));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public FluentResults.Result DisposeAllPackageData()
|
|
{
|
|
return this.Reset();
|
|
}
|
|
|
|
public bool TryGetConfig<T>(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;
|
|
}
|
|
}
|