- Some work on PluginManagementService, IAssemblyLoaderService and IAssemblyManagementService refactors.

- Fixed mod list sync not checking for zero package diff length.
- Fixed the services provider not being able to inject itself as a dependcency.
- Added UseInternalName data spec to ModConfig.xml
- Changed Basic.Reference.Assemblies to Net80.
This commit is contained in:
MapleWheels
2026-01-29 18:01:45 -05:00
committed by Maplewheels
parent 37e3a195dc
commit 13a9bc443e
13 changed files with 324 additions and 180 deletions
@@ -7,114 +7,124 @@ using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events;
using FluentResults;
using FluentResults.LuaCs;
using ImpromptuInterface.Build;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Toolkit.Diagnostics;
using OneOf;
namespace Barotrauma.LuaCs.Services;
public class PluginManagementService : IPluginManagementService, IAssemblyManagementService
public class PluginManagementService : IAssemblyManagementService
{
private readonly Func<IAssemblyLoaderService.LoaderInitData, IAssemblyLoaderService> _assemblyLoaderServiceFactory;
private readonly ConcurrentDictionary<ContentPackage, (List<IAssemblyResourceInfo> ResourceInfos, IAssemblyLoaderService Loader)> _packageAssemblyResources = new();
private readonly ConcurrentDictionary<ContentPackage, List<IDisposable>> _pluginInstances = new();
private readonly Lazy<IEventService> _eventService;
private readonly ConditionalWeakTable<IAssemblyLoaderService, ContentPackage> _unloadingAssemblyLoaders = new();
private readonly ConditionalWeakTable<Assembly, ConcurrentDictionary<string, Type>> _assemblyTypesCache = new();
#region CSHARP_COMPILATION_OPTIONS
public PluginManagementService(
Func<IAssemblyLoaderService.LoaderInitData,
IAssemblyLoaderService> assemblyLoaderServiceFactory,
Lazy<IEventService> eventService)
{
_assemblyLoaderServiceFactory = assemblyLoaderServiceFactory;
_eventService = eventService;
AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoadedGlobal;
}
private void OnAssemblyLoadedGlobal(object sender, AssemblyLoadEventArgs args)
{
// cache types by name
try
private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
.WithPreprocessorSymbols(new[]
{
var context = AssemblyLoadContext.GetLoadContext(args.LoadedAssembly);
if (context is not IAssemblyLoaderService loaderService)
return;
_eventService.Value.PublishEvent<IEventAssemblyLoaded>(sub => sub.OnAssemblyLoaded(args.LoadedAssembly));
var lookupDict = new ConcurrentDictionary<string, Type>();
foreach (var type in args.LoadedAssembly.GetSafeTypes())
{
lookupDict[type.FullName ?? type.Name] = type;
}
_assemblyTypesCache.AddOrUpdate(args.LoadedAssembly, lookupDict);
}
catch (Exception e)
{
// ignored
return;
}
}
#if SERVER
"SERVER"
#elif CLIENT
"CLIENT"
#else
"UNDEFINED"
#endif
#if DEBUG
,"DEBUG"
#endif
});
private int _isDisposed = 0;
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
#if WINDOWS
private const string PLATFORM_TARGET = "Windows";
#elif OSX
private const string PLATFORM_TARGET = "OSX";
#elif LINUX
private const string PLATFORM_TARGET = "Linux";
#endif
#if CLIENT
private const string ARCHITECTURE_TARGET = "Client";
#elif SERVER
private const string ARCHITECTURE_TARGET = "Server";
#endif
private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithMetadataImportOptions(MetadataImportOptions.All)
#if DEBUG
.WithOptimizationLevel(OptimizationLevel.Debug)
#else
.WithOptimizationLevel(OptimizationLevel.Release)
#endif
.WithAllowUnsafe(true);
private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText(
new StringBuilder()
.AppendLine("using System.Reflection;")
.AppendLine("using Barotrauma;")
.AppendLine("using System.Runtime.CompilerServices;")
.AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]")
#if CLIENT
.AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
#elif SERVER
.AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
#endif
.ToString(),
ScriptParseOptions);
#endregion
#region Disposal
public void Dispose()
{
throw new NotImplementedException();
}
public bool IsDisposed { get; }
public FluentResults.Result Reset()
{
if (IsDisposed)
return FluentResults.Result.Fail($"{nameof(PluginManagementService)} is disposed!");
return FluentResults.Result.Fail("not implemented");
throw new NotImplementedException();
}
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(bool includeInterfaces = false,
bool includeAbstractTypes = false, bool includeDefaultContext = true)
#endregion
private IServicesProvider _serviceProvider;
private IAssemblyLoaderService.IFactory _assemblyLoaderFactory;
private IStorageService _storageService;
private ILoggerService _logger;
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
private readonly AsyncReaderWriterLock _operationsLock = new();
public PluginManagementService(
IServicesProvider serviceProvider,
IAssemblyLoaderService.IFactory assemblyLoaderFactory,
IStorageService storageService,
ILoggerService logger)
{
var builder = ImmutableArray.CreateBuilder<Type>();
if (this._packageAssemblyResources.Any())
{
foreach (var resource in this._packageAssemblyResources
.Where(res => !res.Value.Loader.IsReferenceOnlyMode))
{
builder.AddRange(resource.Value.Loader.Assemblies
.SelectMany(assembly => assembly.GetSafeTypes())
.Where(type => type.IsAssignableTo(typeof(T)))
.Where(type => includeInterfaces || !type.IsInterface)
.Where(type => includeAbstractTypes || !type.IsAbstract));
}
}
if (includeDefaultContext)
{
builder.AddRange(AssemblyLoadContext.Default.Assemblies
.SelectMany(assembly => assembly.GetSafeTypes())
.Where(type => type.IsAssignableTo(typeof(T)))
.Where(type => includeInterfaces || !type.IsInterface)
.Where(type => includeAbstractTypes || !type.IsAbstract));
}
return builder.Count == 0
? FluentResults.Result.Fail($"Failed to find any types that implement {typeof(T).Name})")
: FluentResults.Result.Ok(builder.ToImmutable());
Guard.IsNotNull(serviceProvider, nameof(serviceProvider));
_serviceProvider = serviceProvider;
_assemblyLoaderFactory = assemblyLoaderFactory;
_storageService = storageService;
_logger = logger;
}
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = true,
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(bool includeInterfaces = false, bool includeAbstractTypes = false,
bool includeDefaultContext = true)
{
throw new NotImplementedException();
}
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
bool includeDefaultContext = true)
{
if (includeDefaultContext)
@@ -144,91 +154,197 @@ public class PluginManagementService : IPluginManagementService, IAssemblyManage
return null;
}
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{
IService.CheckDisposed(this);
if (resources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)}: The resources list is empty!");
}
return FluentResults.Result.Fail("not implemented");
}
public ImmutableArray<Result<(Type, T)>> ActivateTypeInstances<T>(ImmutableArray<Type> types, bool serviceInjection = true,
bool hostInstanceReference = false) where T : IDisposable
{
throw new NotImplementedException();
}
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
{
if (resources.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)} The resource list is empty.)");
}
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var orderedContentPacks = resources.GroupBy(res => res.OwnerPackage)
.OrderBy(res => resources.FindIndex(r2 => r2.OwnerPackage == res.Key))
.ToImmutableArray();
var result = new FluentResults.Result();
return FluentResults.Result.Fail($"{nameof(LoadAssemblyResources)}: Not Implemented!");
throw new NotImplementedException();
foreach (var contentPack in orderedContentPacks)
{
LoadBinaries(contentPack);
LoadAndCompileScriptAssemblies(contentPack);
}
return result;
// helper methods
void LoadBinaries(IGrouping<ContentPackage,IAssemblyResourceInfo> contentPackRes)
{
var binaries = contentPackRes.Where(cRes => !cRes.IsScript)
.OrderBy(bin => bin.LoadPriority)
.SelectMany(bin => bin.FilePaths)
.ToImmutableArray();
if (binaries.IsDefaultOrEmpty)
{
return;
}
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: false,
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
var dependencyPaths = binaries
.Select(bin => System.IO.Path.GetDirectoryName(bin.FullPath))
.Distinct()
.ToImmutableArray();
foreach (var binResource in binaries)
{
var res = assemblyLoader.LoadAssemblyFromFile(binResource.FullPath, dependencyPaths);
result.WithReasons(res.Reasons);
#if DEBUG
return ImmutableArray<Result<(Type, T)>>.Empty;
_logger.LogResults(res.ToResult());
#endif
if (res.IsFailed)
{
_logger.LogResults(res.ToResult());
}
}
}
void LoadAndCompileScriptAssemblies(IGrouping<ContentPackage, IAssemblyResourceInfo> contentPackRes)
{
var scripts = contentPackRes.Where(cRes => cRes.IsScript)
.OrderBy(scr => scr.LoadPriority)
.Select(scr => (scr.FriendlyName, scr.FilePaths, scr.UseInternalAccessName))
.GroupBy(scr => scr.FriendlyName)
.ToImmutableArray();
if (scripts.IsDefaultOrEmpty)
{
return;
}
var metadataReferences = GetMetadataReferences();
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
new IAssemblyLoaderService.LoaderInitData(
InstanceId: Guid.NewGuid(),
contentPackRes.Key.Name,
IsReferenceMode: false,
OwnerPackage: contentPackRes.Key,
OnUnload: OnAssemblyLoaderUnloading,
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
)));
// create syntax trees
var syntaxTreesBuilder = ImmutableArray.CreateBuilder<SyntaxTree>();
foreach (var resourceInfo in contentPackRes)
{
if (resourceInfo.FilePaths.IsDefaultOrEmpty)
{
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAndCompileScriptAssemblies)} The resource list is empty for package {resourceInfo.OwnerPackage}.");
}
var loadRes = GetSourceFilesText(resourceInfo.FilePaths);
if (loadRes.IsFailed)
{
_logger.LogResults(loadRes.ToResult());
continue;
}
CancellationToken token = CancellationToken.None;
syntaxTreesBuilder.Add(SyntaxFactory.ParseSyntaxTree(
text: loadRes.Value,
options: ScriptParseOptions,
path: null,
encoding: Encoding.Default,
cancellationToken: token
));
}
throw new NotImplementedException();
}
Result<string> GetSourceFilesText(ImmutableArray<ContentPath> resourceInfoFilePaths)
{
if (_storageService.LoadPackageTextFiles(resourceInfoFilePaths) is not { IsDefaultOrEmpty: false } res)
{
_logger.LogError($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePaths.First().ContentPackage?.Name}.");
return FluentResults.Result.Fail($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePaths.First().ContentPackage?.Name}.");
}
var loadRes = new FluentResults.Result();
StringBuilder sb = new StringBuilder();
foreach ((ContentPath Path, Result<string> FileResult) loadResult in res)
{
if (loadResult.FileResult.IsFailed)
{
loadRes.WithErrors(loadResult.FileResult.Errors);
continue;
}
sb.AppendLine(loadResult.FileResult.Value);
}
if (loadRes.IsFailed)
{
return loadRes;
}
return sb.ToString();
}
IEnumerable<MetadataReference> GetMetadataReferences()
{
return Basic.Reference.Assemblies.Net80.References.All;
}
}
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly arg1, string arg2)
{
throw new NotImplementedException();
}
private Assembly OnAssemblyLoaderResolvingManaged(IAssemblyLoaderService arg1, AssemblyName arg2)
{
throw new NotImplementedException();
}
private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader)
{
throw new NotImplementedException();
}
public FluentResults.Result UnloadManagedAssemblies()
{
var res = new FluentResults.Result();
// cleanup managed plugins
if (_pluginInstances.Any())
{
foreach (var packageInstances in _pluginInstances)
{
if (!packageInstances.Value.Any())
continue;
foreach (var disposable in packageInstances.Value)
{
try
{
disposable.Dispose();
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this));
}
}
}
_pluginInstances.Clear();
}
_assemblyTypesCache.Clear();
// cleanup running assembly contexts
if (_packageAssemblyResources.Any())
{
foreach (var resource in _packageAssemblyResources.ToImmutableDictionary())
{
if (resource.Value.Loader is not null)
{
try
{
resource.Value.Loader.Dispose();
_unloadingAssemblyLoaders.AddOrUpdate(resource.Value.Loader, resource.Key);
_packageAssemblyResources.TryRemove(resource);
_packageAssemblyResources.TryAdd(resource.Key, (resource.Value.ResourceInfos, null));
}
catch (Exception e)
{
res = res.WithError(new ExceptionalError(e)
.WithMetadata(MetadataType.ExceptionObject, this));
}
}
}
}
return res.WithSuccess($"Unloading of managed assemblies started successfully,");
return FluentResults.Result.Fail($"{nameof(UnloadManagedAssemblies)}: Not Implemented.");
throw new NotImplementedException();
}
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)
{
throw new NotImplementedException();
}
public ImmutableArray<MetadataReference> GetDefaultMetadataReferences(bool includeDefaultContext = true)
{
throw new NotImplementedException();
}
public ImmutableArray<IAssemblyLoaderService> AssemblyLoaderServices { get; }
}