From 26b657a96fd942960b0d235f78bf853f9c437e0b Mon Sep 17 00:00:00 2001 From: Maplewheels Date: Sun, 28 Dec 2025 07:23:58 -0500 Subject: [PATCH] [In-Progress] Plugin system rewrite. Game starts up, runs until unimplemented functions are reached without errors. --- .../LuaCs/Data/ServicesConfigData.cs | 26 ++- .../SharedSource/LuaCs/LuaCsSetup.cs | 10 +- .../LuaCs/Services/PluginManagementService.cs | 210 ++++++++---------- .../LuaCs/Services/ServicesProvider.cs | 2 +- .../_Interfaces/IAssemblyManagementService.cs | 28 +-- .../_Interfaces/IPluginManagementService.cs | 26 +-- .../LuaCs/_Plugins/AssemblyLoader.cs | 28 ++- .../LuaCs/_Plugins/IAssemblyLoaderService.cs | 30 ++- 8 files changed, 177 insertions(+), 183 deletions(-) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs index f3e3bab2d..b5ee25eb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ServicesConfigData.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Security.AccessControl; +using Barotrauma.LuaCs.Services; using Barotrauma.Networking; using FluentResults; @@ -14,7 +15,8 @@ namespace Barotrauma.LuaCs.Data; // --- Storage Service -public interface IStorageServiceConfig +// TODO: Configs should not be services, add new registration path for them. +public interface IStorageServiceConfig : IService { string LocalModsDirectory { get; } string WorkshopModsDirectory { get; } @@ -176,10 +178,17 @@ public record StorageServiceConfig : IStorageServiceConfig, IStorageServiceConfi return result.WithSuccess($"Whitelist updated."); } + + public void Dispose() + { + // cannot be disposed. + } + + public bool IsDisposed => false; } // --- Config Service -public interface IConfigServiceConfig +public interface IConfigServiceConfig : IService { string LocalConfigPathPartial { get; } string FileNamePattern { get; } @@ -189,11 +198,16 @@ public record ConfigServiceConfig : IConfigServiceConfig { public string LocalConfigPathPartial => $"/Config/{FileNamePattern}.xml"; public string FileNamePattern => ""; + public void Dispose() + { + // ignored + } + public bool IsDisposed => false; } // --- Lua Scripts Service -public interface ILuaScriptServicesConfig +public interface ILuaScriptServicesConfig : IService { bool SafeLuaIOEnabled { get; } bool UseCaching { get; } @@ -203,4 +217,10 @@ public record LuaScriptServicesConfig : ILuaScriptServicesConfig { public bool SafeLuaIOEnabled => true; public bool UseCaching => true; + public void Dispose() + { + // ignored + } + + public bool IsDisposed => false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 4996dcc00..691486a54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -12,6 +12,7 @@ using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs.Services; using Barotrauma.LuaCs.Services.Compatibility; using Barotrauma.LuaCs.Services.Processing; +using Barotrauma.LuaCs.Services.Safe; using Barotrauma.Networking; using FluentResults; using ImpromptuInterface; @@ -47,6 +48,7 @@ namespace Barotrauma _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); // TODO: IConfigService @@ -64,6 +66,12 @@ namespace Barotrauma _servicesProvider.RegisterServiceType, ModConfigService>(ServiceLifetime.Transient); _servicesProvider.RegisterServiceType(ServiceLifetime.Transient); + // service config data + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + + // gen IL _servicesProvider.Compile(); } @@ -702,7 +710,7 @@ namespace Barotrauma { EventService.ClearAllSubscribers(); LuaScriptManagementService.UnloadActiveScripts(); - PluginManagementService.UnloadAllAssemblyResources(); + PluginManagementService.UnloadManagedAssemblies(); SubscribeToLuaCsEvents(); if (IsCodeRunning) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs index 5289e946a..d3e9d8a8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs @@ -10,96 +10,116 @@ using System.Runtime.Loader; using System.Threading; using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Events; using FluentResults; using Microsoft.CodeAnalysis; +using OneOf; namespace Barotrauma.LuaCs.Services; public class PluginManagementService : IPluginManagementService, IAssemblyManagementService { + private readonly Func _assemblyLoaderServiceFactory; + private readonly ConcurrentDictionary ResourceInfos, IAssemblyLoaderService Loader)> _packageAssemblyResources; + private readonly ConcurrentDictionary> _pluginInstances; + private readonly Lazy _eventService; + private readonly ConditionalWeakTable _unloadingAssemblyLoaders; + private readonly ConditionalWeakTable> _assemblyTypesCache; + + public PluginManagementService( + Func assemblyLoaderServiceFactory, + Lazy eventService) + { + _assemblyLoaderServiceFactory = assemblyLoaderServiceFactory; + _eventService = eventService; + AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoadedGlobal; + } + + private void OnAssemblyLoadedGlobal(object sender, AssemblyLoadEventArgs args) + { + // cache types by name + try + { + var context = AssemblyLoadContext.GetLoadContext(args.LoadedAssembly); + if (context is not IAssemblyLoaderService loaderService) + return; + _eventService.Value.PublishEvent(sub => sub.OnAssemblyLoaded(args.LoadedAssembly)); + var lookupDict = new ConcurrentDictionary(); + foreach (var type in args.LoadedAssembly.GetSafeTypes()) + { + lookupDict[type.FullName ?? type.Name] = type; + } + _assemblyTypesCache.AddOrUpdate(args.LoadedAssembly, lookupDict); + } + catch (Exception e) + { + // ignored + return; + } + } + + private int _isDisposed = 0; public bool IsDisposed { get => ModUtils.Threading.GetBool(ref _isDisposed); - set => ModUtils.Threading.SetBool(ref _isDisposed, value); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } - private int _isDisposed; - - private readonly ReaderWriterLockSlim _operationsLock = new(LockRecursionPolicy.SupportsRecursion); - - private readonly ConcurrentDictionary _assemblyServices = new(); - private readonly ConcurrentDictionary _resourceData = new(); - private readonly Lazy _eventService; - private readonly Func _assemblyServiceFactory; - private ImmutableDictionary _cachedTypes = null; - private ImmutableDictionary DefaultTypeCache => _cachedTypes ??= AssemblyLoadContext.Default.Assemblies - .SelectMany(ass => ass.GetSafeTypes()).ToImmutableDictionary(type => type.FullName, type => type); - - public bool IsResourceLoaded(T resource) where T : IAssemblyResourceInfo + public void Dispose() { - ((IService)this).CheckDisposed(); - return _resourceData.ContainsKey(resource); + throw new NotImplementedException(); } - public Result> GetImplementingTypes(string namespacePrefix = null, bool includeInterfaces = false, + public FluentResults.Result Reset() + { + if (IsDisposed) + return FluentResults.Result.Fail($"{nameof(PluginManagementService)} is disposed!"); + + throw new NotImplementedException(); + } + + public Result> GetImplementingTypes(bool includeInterfaces = false, bool includeAbstractTypes = false, bool includeDefaultContext = true) { - ((IService)this).CheckDisposed(); - var types = ImmutableArray.CreateBuilder(); - _operationsLock.EnterReadLock(); - try - { - if (AssemblyLoaderServices.Any()) - { - types.AddRange(AssemblyLoaderServices - .SelectMany(als => als.UnsafeGetTypesInAssemblies()) - .Where(t => t is not null) - .Where(type => typeof(T).IsAssignableFrom(type)) - .Where(type => includeInterfaces || !type.IsInterface) - .Where(type => includeAbstractTypes || !type.IsAbstract) - .Where(type => namespacePrefix is not null && type.FullName is not null && type.FullName.StartsWith(namespacePrefix))); - } + var builder = ImmutableArray.CreateBuilder(); - if (includeDefaultContext) - { - types.AddRange(AssemblyLoadContext.Default.Assemblies - .SelectMany(ass => ass.GetSafeTypes()) - .Where(t => t is not null) - .Where(type => typeof(T).IsAssignableFrom(type)) - .Where(type => includeInterfaces || !type.IsInterface) - .Where(type => includeAbstractTypes || !type.IsAbstract) - .Where(type => namespacePrefix is not null && type.FullName is not null && type.FullName.StartsWith(namespacePrefix))); - } - - return types.MoveToImmutable(); - } - finally + if (this._packageAssemblyResources.Any()) { - _operationsLock.ExitReadLock(); + 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()); } - public Type GetType(string typeName) + public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, + bool includeDefaultContext = true) { - ((IService)this).CheckDisposed(); - _operationsLock.EnterReadLock(); - try + if (includeDefaultContext) { - if (DefaultTypeCache.TryGetValue(typeName, out var type)) - return type; - if (AssemblyLoaderServices.None()) - return null; - foreach (var loaderService in AssemblyLoaderServices) - { - if (loaderService.GetTypeInAssemblies(typeName) is { IsSuccess: true, Value: not null } ret) - return ret.Value; - } - return null; - } - finally - { - _operationsLock.ExitReadLock(); + var type = Type.GetType(typeName, false); } + + // TODO: implement by-ref type resolution + throw new NotImplementedException(); } public Result> LoadAssemblyResources(ImmutableArray resource) @@ -113,72 +133,20 @@ public class PluginManagementService : IPluginManagementService, IAssemblyManage throw new NotImplementedException(); } - public FluentResults.Result UnloadHostedReferences() + public FluentResults.Result UnloadManagedAssemblies() { throw new NotImplementedException(); } - public FluentResults.Result UnloadAllAssemblyResources() + public Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts) { throw new NotImplementedException(); } - public Result GetLoadedAssembly(string assemblyName, in Guid[] excludedContexts) + public ImmutableArray GetDefaultMetadataReferences(bool includeDefaultContext = true) { - ((IService)this).CheckDisposed(); - _operationsLock.EnterReadLock(); - try - { - foreach (var (guid, context) in _assemblyServices) - { - if (excludedContexts.Length > 0 && excludedContexts.Contains(guid)) - continue; - if (context.GetAssemblyByName(assemblyName) is { IsSuccess: true, Value: not null } ret) - return ret.Value; - } - return FluentResults.Result.Fail($"Could not find assembly {assemblyName}"); - } - finally - { - _operationsLock.ExitReadLock(); - } - } - - public Result GetLoadedAssembly(AssemblyName assemblyName, in Guid[] excludedContexts) - => GetLoadedAssembly(assemblyName.FullName, excludedContexts); - - public ImmutableArray GetDefaultMetadataReferences() => - Basic.Reference.Assemblies.Net60.References.All.Select(Unsafe.As).ToImmutableArray(); - - public ImmutableArray GetAddInContextsMetadataReferences() - { - ((IService)this).CheckDisposed(); - _operationsLock.EnterReadLock(); - try - { - if (_assemblyServices.IsEmpty) - return ImmutableArray.Empty; - var builder = ImmutableArray.CreateBuilder(); - foreach (var context in _assemblyServices.Values) - builder.AddRange(context.AssemblyReferences); - return builder.ToImmutable(); - } - finally - { - _operationsLock.ExitReadLock(); - } + throw new NotImplementedException(); } public ImmutableArray AssemblyLoaderServices { get; } - - public void Dispose() - { - // TODO release managed resources here - throw new NotImplementedException(); - } - - public FluentResults.Result Reset() - { - throw new NotImplementedException(); - } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs index 49d45cb75..9b2c6420b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs @@ -134,7 +134,7 @@ public class ServicesProvider : IServicesProvider service = ServiceContainer.TryGetInstance(); return service is not null; } - catch + catch { service = null; return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs index b347a301f..2499d55f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs @@ -6,41 +6,33 @@ using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using OneOf; + // ReSharper disable InconsistentNaming namespace Barotrauma.LuaCs.Services; public interface IAssemblyManagementService : IReusableService { - /// - /// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied. - /// - /// The fully-qualified assembly name. - /// Guids of excluded contexts. - /// On Success: The assembly.
On Failure: nothing.
- FluentResults.Result GetLoadedAssembly(string assemblyName, in Guid[] excludedContexts); + /// /// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied. /// /// The assembly info. /// Guids of excluded contexts. /// On Success: The assembly.
On Failure: nothing.
- FluentResults.Result GetLoadedAssembly(AssemblyName assemblyName, in Guid[] excludedContexts); + FluentResults.Result GetLoadedAssembly(OneOf assemblyName, in Guid[] excludedContexts); /// - /// Gets the assembly collection for the BCL and base game assemblies. + /// Gets all for all service-managed assemblies. /// - /// collection, if any are found. Returns an empty collection otherwise. - ImmutableArray GetDefaultMetadataReferences(); + /// collection for all service-managed, and default if selected, assemblies, if any are found. Returns an empty collection otherwise. + ImmutableArray GetDefaultMetadataReferences(bool includeDefaultContext = true); /// - /// Gets the assembly collection for all add-in assemblies loaded. - /// - /// collection, if any are found. Returns an empty collection otherwise. - ImmutableArray GetAddInContextsMetadataReferences(); - - /// - /// + /// Returns all active, managed assembly loaders. /// ImmutableArray AssemblyLoaderServices { get; } + + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs index 6319573bb..dbb87bdf9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs @@ -10,33 +10,27 @@ namespace Barotrauma.LuaCs.Services; public interface IPluginManagementService : IReusableService { /// - /// Checks if the supplied resource is currently loaded. + /// Gets all types in searched that implement the type supplied. /// - /// The resource to check. - /// - bool IsResourceLoaded(T resource) where T : IAssemblyResourceInfo; - - /// - /// - /// - /// /// /// /// /// /// FluentResults.Result> GetImplementingTypes( - string namespacePrefix = null, bool includeInterfaces = false, bool includeAbstractTypes = false, bool includeDefaultContext = true); /// - /// Tries to get the Type given the fully qualified name. + /// Tries to find the type given the fully qualified name and filters. /// /// + /// + /// + /// /// - Type GetType(string typeName); + Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true); /// /// Loads the provided assembly resources in the order of their dependencies and intra-mod priority load order. @@ -56,11 +50,9 @@ public interface IPluginManagementService : IReusableService IReadOnlyList> ActivateTypeInstances(ImmutableArray types, bool serviceInjection = true, bool hostInstanceReference = false) where T : IDisposable; - FluentResults.Result UnloadHostedReferences(); - /// - /// Tries to gracefully unload all hosted plugin references + /// Unloads all managed , , and s. /// - /// - FluentResults.Result UnloadAllAssemblyResources(); + /// Success of the operation.
Note: does not guarantee .NET runtime assembly unloading success.
+ FluentResults.Result UnloadManagedAssemblies(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs index ce6334379..a7cebbd5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -25,6 +25,7 @@ namespace Barotrauma.LuaCs.Services; public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { public Guid Id { get; init; } + public ContentPackage OwnerPackage { get; private set; } public bool IsReferenceOnlyMode { get; init; } public bool IsDisposed { @@ -55,8 +56,8 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService //internal private readonly IAssemblyManagementService _assemblyManagementService; - private readonly Action _onUnload; - private readonly Func _onResolvingManaged; + private readonly Action _onUnload; + private readonly Func _onResolvingManaged; private readonly Func _onResolvingUnmanagedDll; private readonly ConcurrentDictionary _dependencyResolvers = new(); private readonly ConcurrentDictionary _loadedAssemblyData = new(); @@ -64,16 +65,16 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService private readonly ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit private readonly ThreadLocal _isResolvingNative = new(static () => false); - public AssemblyLoader( - IAssemblyManagementService assemblyManagementService, - Guid id, string name, - bool isReferenceOnlyMode, - Action onUnload = null) - : base(isCollectible: true, name: name) + public AssemblyLoader(IAssemblyLoaderService.LoaderInitData initData) + : base(isCollectible: true, name: initData.Name) { - _assemblyManagementService = assemblyManagementService; - Id = id; - IsReferenceOnlyMode = isReferenceOnlyMode; + _assemblyManagementService = initData.AssemblyManagementService; + Id = initData.InstanceId; + IsReferenceOnlyMode = initData.IsReferenceMode; + this._onUnload = initData.OnUnload; + this._onResolvingManaged = initData.OnResolvingManaged; + this._onResolvingUnmanagedDll = initData.OnResolvingUnmanagedDll; + this.OwnerPackage = initData.OwnerPackage; base.Unloading += OnUnload; base.Resolving += OnResolvingManagedAssembly; base.ResolvingUnmanagedDll += OnResolvingUnmanagedDll; @@ -138,6 +139,9 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService if (_isResolving.Value) return null; + + if (assemblyLoadContext != this) + return null; AreOperationRunning = true; _isResolving.Value = true; @@ -166,7 +170,7 @@ public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService { try { - return _onResolvingManaged(assemblyLoadContext, assemblyName); + return _onResolvingManaged(this, assemblyName); } catch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs index 6f4dc1159..071effa1a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; @@ -13,17 +14,26 @@ namespace Barotrauma.LuaCs; public interface IAssemblyLoaderService : IService { /// - /// Assembly loader factory for DI registration. + /// Constructor record for instancing. /// - /// The assembly hosting management service. - /// The event service for publishing. - /// The referencing ID. Intended to be used to distinguish between instances. - /// The name of the friendly name instance, used for error messages. - /// Loaded assemblies are not intended for execution, just MetadataReferences. - delegate IAssemblyLoaderService AssemblyLoaderDelegate( - IAssemblyManagementService assemblyManagementService, - IEventService eventService, Guid id, string name, - bool isReferenceOnlyMode, Action onUnload); + /// + /// + /// + /// Assemblies and Types in this context are for only. + /// Execution of assembly data is forbidden. + /// + /// + /// + /// + public record LoaderInitData( + [Required][NotNull] IAssemblyManagementService AssemblyManagementService, + [Required] Guid InstanceId, + [Required][NotNull] string Name, + [Required] bool IsReferenceMode, + ContentPackage OwnerPackage, + Action OnUnload, + Func OnResolvingManaged, + Func OnResolvingUnmanagedDll); /// /// ID for this instance.