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.
687 lines
27 KiB
C#
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
|
|
}
|