Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs
MapleWheels 6880e5e9ee [Milestone] AssemblyLoader completed.
Details:
- Assembly Mgmt Service for loading now a separate interface, not intended for normal use.
- Assembly Loader work; implemented custom dictionary key and table.
- Assembly loading work.
- EventService completed.
- Moved assembly extensions to ModUtils.cs
- Work to event service.
NetworkService work
- Added ImpromptuInterfaces package.
- Networking Service work to support NetVars
- Event Service
- Added assemblies references package for script compilation. Updated Roslyn version for compatibility.
- Package Loading work.
Swap Harmony to HarmonyX
- More refactor conversion to FluentResults.
- Updated StylesService to return Results.
- Refactor of PackageService partially complete.
- Made IService.Reset() required to return a Result.
- Moved plugin/assembly related code to their own folder (same namespace).
- Updated interfaces to reflect the use of Result<T>.
- Partial refactor, incomplete.
- Added 'FluentResults' so we can stop using cursed Exception-based flow control in loading code.
- Added 'OneOf' nuget package: https://github.com/mcintyre321/OneOf
for the implementation of the Optional<T> pattern and complex discrete return types instead of cursed enums (see current AssemblyManager.cs).
- Reapplied old branch changes.
2026-02-07 20:10:26 -05:00

687 lines
27 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Services.Processing;
using FluentResults;
using FluentResults.LuaCs;
using OneOf;
namespace Barotrauma.LuaCs.Services;
public partial class PackageService : IPackageService
{
private readonly ReaderWriterLockSlim _operationsUsageLock = new();
// only stops race conditions for pointer access
// mod config / package scanners/parsers
private readonly Lazy<IModConfigParserService> _configParserService;
private readonly Lazy<ILuaScriptService> _luaScriptService;
private readonly Lazy<ILocalizationService> _localizationService;
private readonly Lazy<IPluginService> _pluginService;
private readonly Lazy<IConfigService> _configService;
private readonly IPackageManagementService _packageManagementService;
private readonly IStorageService _storageService;
private readonly ILoggerService _loggerService;
// .ctor in server source and client source
// state monitors
private int _configsLoaded, _localizationsLoaded, _luaScriptsLoaded, _pluginsLoaded, _isDisposed;
private int _loadingOperationsRunning;
private int _isEnabledInModList;
public bool ConfigsLoaded
{
get => ModUtils.Threading.GetBool(ref _configsLoaded);
private set => ModUtils.Threading.SetBool(ref _configsLoaded, value);
}
public bool LocalizationsLoaded
{
get => ModUtils.Threading.GetBool(ref _localizationsLoaded);
private set => ModUtils.Threading.SetBool(ref _localizationsLoaded, value);
}
public bool LuaScriptsLoaded
{
get => ModUtils.Threading.GetBool(ref _luaScriptsLoaded);
private set => ModUtils.Threading.SetBool(ref _luaScriptsLoaded, value);
}
public bool PluginsLoaded
{
get => ModUtils.Threading.GetBool(ref _pluginsLoaded);
private set => ModUtils.Threading.SetBool(ref _pluginsLoaded, value);
}
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
private bool LoadingOperationsRunning
{
get => Interlocked.CompareExchange(ref _loadingOperationsRunning, 0, 0) > 0;
set // we use the set as our inc/decr
{
if (value)
{
Interlocked.Add(ref _loadingOperationsRunning, 1);
}
else
{
Interlocked.Add(ref _loadingOperationsRunning, -1);
}
}
}
#region Member: ContentPackage
private readonly ReaderWriterLockSlim _packageAccessLock = new();
private ContentPackage _package;
public ContentPackage Package
{
get
{
_packageAccessLock.EnterReadLock();
try
{
return _package;
}
finally
{
_packageAccessLock.ExitReadLock();
}
}
private set
{
_packageAccessLock.EnterWriteLock();
try
{
_package = value;
}
finally
{
_packageAccessLock.ExitWriteLock();
}
}
}
#endregion
#region DataContracts
#region Member: ModConfigInfo
private readonly ReaderWriterLockSlim _modConfigUsageLock = new();
private IModConfigInfo _modConfigInfo;
public IModConfigInfo ModConfigInfo
{
get
{
_modConfigUsageLock.EnterReadLock();
try
{
return _modConfigInfo;
}
finally
{
_modConfigUsageLock.ExitReadLock();
}
}
private set
{
_modConfigUsageLock.EnterWriteLock();
try
{
_modConfigInfo = value;
}
finally
{
_modConfigUsageLock.ExitWriteLock();
}
}
}
public bool IsEnabledInModList
{
get => ModUtils.Threading.GetBool(ref _isEnabledInModList);
private set => ModUtils.Threading.SetBool(ref _isEnabledInModList, value);
}
#endregion
public ImmutableArray<CultureInfo> SupportedCultures => ModConfigInfo?.SupportedCultures ?? ImmutableArray<CultureInfo>.Empty;
public ImmutableArray<IAssemblyResourceInfo> Assemblies => ModConfigInfo?.Assemblies ?? ImmutableArray<IAssemblyResourceInfo>.Empty;
public ImmutableArray<ILocalizationResourceInfo> Localizations => ModConfigInfo?.Localizations ?? ImmutableArray<ILocalizationResourceInfo>.Empty;
public ImmutableArray<ILuaResourceInfo> LuaScripts => ModConfigInfo?.LuaScripts ?? ImmutableArray<ILuaResourceInfo>.Empty;
public ImmutableArray<IConfigResourceInfo> Configs => ModConfigInfo?.Configs ?? ImmutableArray<IConfigResourceInfo>.Empty;
public ImmutableArray<IConfigProfileResourceInfo> ConfigProfiles => ModConfigInfo?.ConfigProfiles ?? ImmutableArray<IConfigProfileResourceInfo>.Empty;
#endregion
#region PublicAPI
public FluentResults.Result LoadResourcesInfo(LoadablePackage cpackage)
{
if (cpackage.Package == null)
{
return FluentResults.Result.Fail(new Error($"{nameof(LoadResourcesInfo)}: Package is null!")
.WithMetadata(MetadataType.ExceptionObject,this)
.WithMetadata(MetadataType.RootObject, cpackage));
}
ContentPackage package = cpackage.Package;
_operationsUsageLock.EnterWriteLock();
LoadingOperationsRunning = true;
try
{
if (IsDisposed)
{
return FluentResults.Result.Fail(
new Error("Service is disposed.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package));
}
var res = _configParserService.Value.BuildConfigForPackage(package);
if (res.IsFailed)
{
return FluentResults.Result.Fail(res.Errors)
.WithError(new Error("PackageService failed to load ModConfigInfo")
.WithMetadata(MetadataType.ExceptionObject, _configParserService)
.WithMetadata(MetadataType.RootObject, package));
}
this.ModConfigInfo = res.Value;
this.IsEnabledInModList = cpackage.IsEnabled;
return FluentResults.Result.Ok();
}
catch (Exception e)
{
return FluentResults.Result.Fail(new Error(e.Message)
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, package)
.WithMetadata(MetadataType.StackTrace, e.StackTrace));
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitWriteLock();
}
}
public FluentResults.Result LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT0(assembliesInfo)) is { IsFailed: true } failed)
{
return failed;
}
// Order these assemblies by internal dependencies
ImmutableArray<IAssemblyResourceInfo> resources;
if (ignoreDependencySorting)
{
resources = assembliesInfo.Assemblies;
}
else // sort by load order
{
resources = assembliesInfo.Assemblies
.OrderByDescending(a => a.LoadPriority)
.ToImmutableArray();
}
// Try loading them, throw on failure.
if (_pluginService.Value.LoadAndInstanceTypes<IAssemblyPlugin>(resources, true, out var instancedTypes) is { IsFailed: true} failed2)
{
return failed2.WithError(new Error($"{nameof(LoadPlugins)}: Failed to load plugins for {this.Package.Name}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, assembliesInfo));
}
PluginsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public FluentResults.Result LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT1(localizationsInfo)) is { IsFailed: true } failed)
{
return failed;
}
if (_localizationService.Value.LoadLocalizations(localizationsInfo.Localizations) is { IsFailed: true} failed2)
{
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load localizations")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, localizationsInfo));
}
LocalizationsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public FluentResults.Result AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT4(luaScriptsInfo)) is { IsFailed: true } failed)
{
return failed;
}
if (_luaScriptService.Value.AddScriptFiles(luaScriptsInfo.LuaScripts) is { IsFailed: true} failed2)
{
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load lua scripts.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, luaScriptsInfo));
}
LuaScriptsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public FluentResults.Result LoadConfig(
[NotNull]IConfigsResourcesInfo configsResourcesInfo,
[NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo)
{
_operationsUsageLock.EnterReadLock();
LoadingOperationsRunning = true;
try
{
// register configs
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT2(configsResourcesInfo)) is { IsFailed: true } failed)
{
return failed;
}
if (_configService.Value.AddConfigs(configsResourcesInfo.Configs) is { IsFailed: true} failed2)
{
return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load configs.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, configsResourcesInfo));
}
// register config profiles
if (CheckResourceSanitation(OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo>
.FromT3(configProfilesResourcesInfo)) is { IsFailed: true } failed3)
{
return failed3;
}
if (_configService.Value.AddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles) is { IsFailed: true} failed4)
{
return failed4.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load config profiles.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, configProfilesResourcesInfo));
}
ConfigsLoaded = true;
return FluentResults.Result.Ok();
}
finally
{
LoadingOperationsRunning = false;
_operationsUsageLock.ExitReadLock();
}
}
public void Dispose()
{
/*
* Notes: we need to unload this package from services in the order that the services are dependent on each other.
* Unloading Order: Lua Scripts > Assemblies > Config Profiles > Configs > Styles > Localizations
*/
_operationsUsageLock.EnterWriteLock();
try
{
if (this.Package is null)
{
_loggerService.LogError(
$"Package Service: cannot Dispose of service as ContentPackage and info is not set!");
return;
}
if (this.ModConfigInfo is null)
{
_loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!");
return;
}
/*
* To be graceful, we want to ensure that any async calls and other threads are allowed to be processed before we begin
* disposal to reduce friction with other thread operations, so we release the lock and periodically check it
* to see of other threads have finished operations before cleaning everything up.
*/
IsDisposed = true; // set stop flag, callers should handle exception cases
Interlocked.MemoryBarrier(); //ensure cache states
DateTime timeoutLimit = DateTime.Now.AddSeconds(10);
while (LoadingOperationsRunning)
{
_operationsUsageLock.ExitWriteLock();
Thread.Sleep(1);
_operationsUsageLock.EnterWriteLock();
if (timeoutLimit < DateTime.Now)
{
_loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing.");
break;
}
}
GC.SuppressFinalize(this);
_luaScriptService.Value.RemoveScriptFiles(this.LuaScripts);
_pluginService.Value.DisposePlugins();
_configService.Value.RemoveConfigsProfiles(this.ConfigProfiles);
_configService.Value.RemoveConfigs(this.Configs);
#if CLIENT
_stylesService.Value.UnloadAllStyles();
#endif
_localizationService.Value.Remove(this.Localizations);
ModConfigInfo = null;
Package = null;
}
catch
{
_loggerService.LogError($"Package Service: exception while running Dispose().");
throw;
}
finally
{
_operationsUsageLock.ExitWriteLock();
}
}
public FluentResults.Result Reset()
{
_operationsUsageLock.EnterWriteLock();
try
{
if (this.Package is null)
{
return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ContentPackage and info is not set!")
.WithMetadata(MetadataType.ExceptionDetails, nameof(Reset))
.WithMetadata(MetadataType.ExceptionObject, this));
}
if (this.ModConfigInfo is null)
{
return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ModConfigInfo is not set!")
.WithMetadata(MetadataType.ExceptionDetails, nameof(Reset))
.WithMetadata(MetadataType.ExceptionObject, this));
}
Interlocked.MemoryBarrier(); //ensure cache states
DateTime timeoutLimit = DateTime.Now.AddSeconds(10);
while (LoadingOperationsRunning)
{
_operationsUsageLock.ExitWriteLock();
Thread.Sleep(1);
_operationsUsageLock.EnterWriteLock();
if (timeoutLimit < DateTime.Now)
{
_loggerService.LogError($"Package Service: Dispose() grace time-out reached while waiting for other operations. Continuing.");
break;
}
}
if (LuaScriptsLoaded)
{
_luaScriptService.Value.RemoveScriptFiles(this.LuaScripts);
LuaScriptsLoaded = false;
}
if (PluginsLoaded)
{
_pluginService.Value.DisposePlugins();
PluginsLoaded = false;
}
if (ConfigsLoaded)
{
_configService.Value.RemoveConfigsProfiles(this.ConfigProfiles);
_configService.Value.RemoveConfigs(this.Configs);
ConfigsLoaded = false;
}
if (LocalizationsLoaded)
{
_localizationService.Value.Remove(this.Localizations);
LocalizationsLoaded = false;
}
return FluentResults.Result.Ok();
}
finally
{
_operationsUsageLock.ExitWriteLock();
}
}
#endregion
#region INTERNAL
/// <summary>
/// [Thread Unsafe] Performs sanitation and null checks on resources and returns the results.
/// NOTE: Requires that resource locks be set by the caller.
/// </summary>
/// <param name="resourcesInfos"></param>
/// <returns></returns>
private FluentResults.Result CheckResourceSanitation(
OneOf.OneOf<IAssembliesResourcesInfo, ILocalizationsResourcesInfo,
IConfigsResourcesInfo, IConfigProfilesResourcesInfo, ILuaScriptsResourcesInfo> resourcesInfos)
{
// execute checks based on known types
return resourcesInfos.Match<FluentResults.Result>(
ass => ChecksDispatcher(ass, nameof(ass.Assemblies), nameof(LoadPlugins),
ass.Assemblies, this.Assemblies),
loc => ChecksDispatcher(loc, nameof(loc.Localizations), nameof(LoadLocalizations),
loc.Localizations, this.Localizations),
cfg => ChecksDispatcher(cfg, nameof(cfg.Configs), nameof(LoadConfig),
cfg.Configs, this.Configs),
cfp => ChecksDispatcher(cfp, nameof(cfp.ConfigProfiles), nameof(LoadConfig),
cfp.ConfigProfiles, this.ConfigProfiles),
lua => ChecksDispatcher(lua, nameof(lua.LuaScripts), nameof(AddLuaScripts),
lua.LuaScripts, this.LuaScripts));
/*
* Helper functions
*/
FluentResults.Result ChecksDispatcher<T>(object obj, string resName, string callerName,
ImmutableArray<T> resList, ImmutableArray<T> compareList)
where T : class, IPackageInfo, IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo
{
string errMsg = $"{callerName}: Failed to load {resName}.";
if (DisposeCheck(obj) is { IsFailed: true } failed)
return failed;
if (SanitationChecksCore(obj, resName, callerName) is { IsFailed: true } failed1)
return failed1.WithError(new Error(errMsg));
if (SanitationChecksEnumerable(resList, resName, callerName) is { IsFailed: true } failed2)
return failed2.WithError(new Error(errMsg));
if (DebugCheck(resList, compareList, resName) is {IsFailed: true} failed3)
return failed3.WithError(new Error(errMsg));
return FluentResults.Result.Ok();
}
FluentResults.Result DisposeCheck(object obj)
{
if (IsDisposed)
{
return FluentResults.Result.Fail(new Error($"{nameof(PackageService)}: Tried to load resources when disposed.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, obj));
}
return FluentResults.Result.Ok();
}
FluentResults.Result DebugCheck<T>(ImmutableArray<T> resList, ImmutableArray<T> compareList, string resName)
where T : class, IPackageInfo
{
#if DEBUG
Stack<Error> errors = new();
resList.ForEach(res =>
{
if (!compareList.Contains(res))
{
errors.Push(new Error($"Failed to load {resName} for: {this.Package.Name}")
.WithMetadata(MetadataType.ExceptionDetails, $"Tries to load {resName} resource {res.InternalName} but it is not from this package!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, res));
}
});
if (errors.Count > 0)
{
return FluentResults.Result.Fail(errors).WithError(
new Error($"{nameof(LoadPlugins)}: errors in {resName} resources.")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, this.Package));
}
#endif
return FluentResults.Result.Ok();
}
}
private FluentResults.Result SanitationChecksCore(object obj, string resTypeInfoName, string callerName)
{
Error e = null;
if (obj is null)
{
e = new Error($"{nameof(SanitationChecksCore)}: null checks failed!")
.WithMetadata(MetadataType.ExceptionDetails, "Object is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, new List<string>() { resTypeInfoName, callerName });
}
if (this.Package is null)
{
e = (e ?? new Error($"{nameof(SanitationChecksCore)}: null checks failed!"))
.WithMetadata(MetadataType.ExceptionDetails, "The Package is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.Sources, new List<string>() { resTypeInfoName, callerName });
}
return e is null ? FluentResults.Result.Ok() : FluentResults.Result.Fail(e);
}
private FluentResults.Result SanitationChecksEnumerable<T>(ImmutableArray<T> resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo
{
// Check if list is empty. Nothing more to do.
if (resourceInfos.IsDefaultOrEmpty)
return FluentResults.Result.Ok();
Stack<Error> errors = new();
// Check if all resources in the list are registered to this package, throw if not.
foreach (var resourceInfo in resourceInfos)
{
// ownership checks
if (resourceInfo.OwnerPackage is null)
{
errors.Push(new Error($"Error for resource: {resTypeInfoName}. OwnerPackage is null!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
continue;
}
if (resourceInfo.OwnerPackage != this.Package)
{
errors.Push(new Error($"Error for resource: {resTypeInfoName}. $\"OwnerPackage {{resourceInfo.OwnerPackage?.Name}} is not the same as this package: {{this.Package}}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
continue;
}
if (resourceInfo.Dependencies.IsDefaultOrEmpty)
continue;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (var pdi in resourceInfo.Dependencies)
{
// for clarification: all resources passed to the function should always be loaded.
// unneeded optional resources should be filtered out before the list is sent.
// left this as a reminder :)
/*if (pdi.Optional)
return;*/
if (!_packageManagementService.CheckDependencyLoaded(pdi))
{
errors.Push(new Error($"Dependency missing for resource: {resourceInfo.OwnerPackage.Name}")
.WithMetadata(MetadataType.ExceptionDetails, $"Missing dependency: {pdi.DependencyPackage?.Name ?? (pdi.FallbackPackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.FallbackPackageName)}")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
}
// check runtime platform
if (!_packageManagementService.CheckEnvironmentSupported(resourceInfo))
{
errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current platform!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
// check local culture
if (!_localizationService.Value.IsCurrentCultureSupported(resourceInfo))
{
errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current culture/region!")
.WithMetadata(MetadataType.ExceptionObject, this)
.WithMetadata(MetadataType.RootObject, resourceInfo));
}
}
return errors.Count > 0 ? FluentResults.Result.Fail(errors) : FluentResults.Result.Ok();
}
#endregion
}