OBT/1.2.0(Spring Update)
Sync with Upstream
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Obsolete("Make your class implement IAssemblyPlugin instead.")]
|
||||
public abstract class ACsMod : IAssemblyPlugin
|
||||
{
|
||||
private static List<ACsMod> mods = new List<ACsMod>();
|
||||
[Obsolete("$This does nothing. Stop using it!")]
|
||||
public static List<ACsMod> LoadedMods { get => mods; }
|
||||
|
||||
private const string MOD_STORE = "LocalMods/.modstore";
|
||||
[Obsolete("$This does nothing. Stop using it!")]
|
||||
public static string GetStoreFolder<T>() where T : ACsMod
|
||||
{
|
||||
if (!Directory.Exists(MOD_STORE)) Directory.CreateDirectory(MOD_STORE);
|
||||
var modFolder = $"{MOD_STORE}/{typeof(T)}";
|
||||
if (!Directory.Exists(modFolder)) Directory.CreateDirectory(modFolder);
|
||||
return modFolder;
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Called as soon as plugin loading begins, use this for internal setup only.
|
||||
/// </summary>
|
||||
public virtual void Initialize() { }
|
||||
|
||||
/// <summary>
|
||||
/// Called once all plugins have completed Initialization. Put cross-mod code here.
|
||||
/// </summary>
|
||||
public virtual void OnLoadCompleted() { }
|
||||
|
||||
/// <summary>
|
||||
/// [NotImplemented] Called before vanilla content is loaded. Use to patch Barotrauma classes before they're
|
||||
/// instantiated.
|
||||
/// </summary>
|
||||
public void PreInitPatching() { }
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LuaCsSetup.Instance.Logger.HandleException(e);
|
||||
}
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
public abstract void Stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using FluentResults;
|
||||
using FluentResults.LuaCs;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using OneOf;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)]
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
|
||||
{
|
||||
public class Factory : IAssemblyLoaderService.IFactory
|
||||
{
|
||||
public IAssemblyLoaderService CreateInstance(IAssemblyLoaderService.LoaderInitData initData)
|
||||
{
|
||||
return new AssemblyLoader(initData);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//stateless service
|
||||
}
|
||||
public bool IsDisposed => false;
|
||||
}
|
||||
|
||||
public Guid Id { get; init; }
|
||||
public ContentPackage OwnerPackage { get; private set; }
|
||||
public bool IsReferenceOnlyMode { get; init; }
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
private int _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0.
|
||||
/// </summary>
|
||||
private bool AreOperationRunning
|
||||
{
|
||||
get => Interlocked.CompareExchange(ref _operationsRunning, 0, 0) > 0;
|
||||
set // we use the set as our inc/decr
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Interlocked.Add(ref _operationsRunning, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Add(ref _operationsRunning, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
private int _operationsRunning;
|
||||
|
||||
//internal
|
||||
private readonly Action<IAssemblyLoaderService> _onUnload;
|
||||
private readonly Func<IAssemblyLoaderService, AssemblyName, Assembly> _onResolvingManaged;
|
||||
private readonly Func<Assembly, string, IntPtr> _onResolvingUnmanagedDll;
|
||||
private readonly ConcurrentDictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();
|
||||
private readonly ConcurrentDictionary<AssemblyOrStringKey, AssemblyData> _loadedAssemblyData = new();
|
||||
|
||||
private readonly ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
|
||||
private readonly ThreadLocal<bool> _isResolvingNative = new(static () => false);
|
||||
|
||||
public AssemblyLoader(IAssemblyLoaderService.LoaderInitData initData)
|
||||
: base(isCollectible: true, name: initData.Name)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private IntPtr OnResolvingUnmanagedDll(Assembly invokingAssembly, string assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return 0;
|
||||
|
||||
if (_isResolvingNative.Value)
|
||||
return 0;
|
||||
|
||||
AreOperationRunning = true;
|
||||
_isResolvingNative.Value = true;
|
||||
try
|
||||
{
|
||||
if (!_dependencyResolvers.IsEmpty)
|
||||
{
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveUnmanagedDllToPath(assemblyName);
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
return base.LoadUnmanagedDllFromPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_onResolvingUnmanagedDll is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _onResolvingUnmanagedDll(invokingAssembly, assemblyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
_isResolvingNative.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly OnResolvingManagedAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return null;
|
||||
|
||||
if (_isResolving.Value)
|
||||
return null;
|
||||
|
||||
if (assemblyLoadContext != this)
|
||||
return null;
|
||||
|
||||
AreOperationRunning = true;
|
||||
_isResolving.Value = true;
|
||||
try
|
||||
{
|
||||
if (!_dependencyResolvers.IsEmpty)
|
||||
{
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
return assemblyLoadContext.LoadFromAssemblyPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_onResolvingManaged is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _onResolvingManaged(this, assemblyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
_isResolving.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<MetadataReference> AssemblyReferences
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDisposed || _loadedAssemblyData.IsEmpty)
|
||||
yield return null;
|
||||
AreOperationRunning = true;
|
||||
foreach (var data in _loadedAssemblyData.Values)
|
||||
{
|
||||
if (data.AssemblyReference is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
yield return data.AssemblyReference;
|
||||
}
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (paths.Length == 0)
|
||||
return FluentResults.Result.Ok();
|
||||
var res = new FluentResults.Result();
|
||||
foreach (var path in paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = Path.GetFullPath(path.CleanUpPath());
|
||||
if (!_dependencyResolvers.ContainsKey(p))
|
||||
{
|
||||
_dependencyResolvers[p] = new AssemblyDependencyResolver(p);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
res = res.WithError(new ExceptionalError(ex)
|
||||
.WithMetadata(MetadataType.Sources, path));
|
||||
}
|
||||
}
|
||||
|
||||
if (res.Errors.Any())
|
||||
return FluentResults.Result.Fail(res.Errors);
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
|
||||
bool compileWithInternalAccess,
|
||||
ImmutableArray<SyntaxTree> syntaxTrees,
|
||||
ImmutableArray<MetadataReference> metadataReferences,
|
||||
CSharpCompilationOptions compilationOptions = null)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new Result<Assembly>().WithError(new Error($"The name provided is null!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
if (_loadedAssemblyData.ContainsKey(assemblyName))
|
||||
{
|
||||
return new Result<Assembly>().WithError(
|
||||
new Error($"The name provided is already assigned to an assembly!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
var compilationAssemblyName = compileWithInternalAccess
|
||||
? IAssemblyLoaderService.InternalsAwareAssemblyName
|
||||
: assemblyName;
|
||||
|
||||
compilationOptions ??= new CSharpCompilationOptions(
|
||||
outputKind: OutputKind.DynamicallyLinkedLibrary,
|
||||
optimizationLevel: OptimizationLevel.Release,
|
||||
concurrentBuild: true,
|
||||
reportSuppressedDiagnostics: false,
|
||||
warningLevel: 0,
|
||||
allowUnsafe: true);
|
||||
|
||||
if (!compileWithInternalAccess)
|
||||
{
|
||||
typeof(CSharpCompilationOptions)
|
||||
.GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?.SetValue(compilationOptions,
|
||||
(uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext
|
||||
| (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility
|
||||
| (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks
|
||||
);
|
||||
}
|
||||
|
||||
using var asmMemoryStream = new MemoryStream();
|
||||
var result = CSharpCompilation
|
||||
.Create(compilationAssemblyName, syntaxTrees,
|
||||
metadataReferences, compilationOptions)
|
||||
.Emit(asmMemoryStream);
|
||||
if (!result.Success)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var resultDiagnostic in result.Diagnostics)
|
||||
{
|
||||
if (resultDiagnostic.IsWarningAsError || resultDiagnostic.Severity == DiagnosticSeverity.Error)
|
||||
{
|
||||
//sb.AppendLine($">>> {resultDiagnostic.GetMessage()} | Location: {resultDiagnostic.Location.SourceTree?.GetLineSpan(resultDiagnostic.Location.SourceSpan)} ");
|
||||
sb.AppendLine($"\n{resultDiagnostic}");
|
||||
}
|
||||
}
|
||||
var res = new FluentResults.Result().WithError(
|
||||
new Error($"Package Error: {OwnerPackage.Name}: Compilation failed for assembly {assemblyName}!\n {sb.ToString()}\n")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
asmMemoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray());
|
||||
_loadedAssemblyData[data.Assembly] = data;
|
||||
return new Result<Assembly>().WithSuccess($"Compiled assembly {assemblyName} successful.")
|
||||
.WithValue(data.Assembly);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new FluentResults.Result().WithError(new ExceptionalError(ex)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyName)
|
||||
.WithMetadata(MetadataType.Sources, syntaxTrees));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
|
||||
ImmutableArray<string> additionalDependencyPaths)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (assemblyFilePath.IsNullOrWhiteSpace())
|
||||
return new Result<Assembly>().WithError(new Error($"The path provided is empty."));
|
||||
|
||||
if (additionalDependencyPaths.Any())
|
||||
{
|
||||
var r = AddDependencyPaths(additionalDependencyPaths);
|
||||
if (r.IsFailed)
|
||||
{
|
||||
// we have errors, loading may not work.
|
||||
return FluentResults.Result.Fail(new Error($"Failed to load dependency paths for '{assemblyFilePath}' with paths: {additionalDependencyPaths.Aggregate((s, ac) => $"{ac}| P={s}")}.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyFilePath))
|
||||
.WithErrors(r.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath());
|
||||
string directoryKey = Path.GetDirectoryName(sanitizedFilePath);
|
||||
|
||||
if (directoryKey is null)
|
||||
{
|
||||
return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, sanitizedFilePath));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = LoadFromAssemblyPath(sanitizedFilePath);
|
||||
_loadedAssemblyData[assembly] = new AssemblyData(assembly, assembly.Location);
|
||||
return new Result<Assembly>().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly);
|
||||
}
|
||||
catch (FileNotFoundException fnfe)
|
||||
{
|
||||
// last attempt
|
||||
try
|
||||
{
|
||||
var assemblyName = new AssemblyName(System.IO.Path.GetFileName(sanitizedFilePath));
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
|
||||
return base.LoadFromAssemblyPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return GenerateExceptionReturn(fnfe);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return GenerateExceptionReturn(fnfe);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return GenerateExceptionReturn(e);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
|
||||
FluentResults.Result<Assembly> GenerateExceptionReturn<T>(T exception) where T : Exception
|
||||
{
|
||||
return FluentResults.Result.Fail<Assembly>(new ExceptionalError(exception)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyFilePath)
|
||||
.WithMetadata(MetadataType.ExceptionDetails, exception.Message)
|
||||
.WithMetadata(MetadataType.StackTrace, exception.StackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return FluentResults.Result.Fail(new Error($"Assembly name is empty.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this));
|
||||
}
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(assemblyName, out var data))
|
||||
{
|
||||
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(data.Assembly);
|
||||
}
|
||||
|
||||
// search any assemblies that were background loaded and we're unaware of.
|
||||
foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a)))
|
||||
{
|
||||
if (assembly1.GetName().FullName == assemblyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!assembly1.Location.IsNullOrWhiteSpace())
|
||||
{
|
||||
_loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location);
|
||||
}
|
||||
// we don't have the original byte array so we can't store it.
|
||||
}
|
||||
catch (NotSupportedException nse) // dynamic assembly or location property threw
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(assembly1);
|
||||
}
|
||||
}
|
||||
|
||||
return FluentResults.Result.Fail(new Error($"Assembly named '{ assemblyName }' not found!"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData
|
||||
.SelectMany(kvp => kvp.Value.Types).ToImmutableArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Type> UnsafeGetTypesInAssemblies()
|
||||
{
|
||||
if (IsDisposed)
|
||||
yield return null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.None())
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var assemblyData in _loadedAssemblyData.Values)
|
||||
{
|
||||
foreach (var type in assemblyData.Types)
|
||||
{
|
||||
yield return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<Type> GetTypeInAssemblies(string typeName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.IsEmpty)
|
||||
return FluentResults.Result.Fail(new Error($"No assemblies loaded!"));
|
||||
foreach (var assemblyData in _loadedAssemblyData)
|
||||
{
|
||||
if (assemblyData.Value.TypesByName.TryGetValue(typeName, out var type))
|
||||
return new FluentResults.Result<Type>().WithSuccess($"Found type.").WithValue(type);
|
||||
}
|
||||
return FluentResults.Result.Fail(new Error($"No matching types found for { typeName }!"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return; // we don't want to invoke events twice nor cause strong GC handles.
|
||||
IsDisposed = true;
|
||||
this.Unload();
|
||||
this.DisposeInternal();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~AssemblyLoader()
|
||||
{
|
||||
this.DisposeInternal();
|
||||
}
|
||||
|
||||
private void OnUnload(AssemblyLoadContext context)
|
||||
{
|
||||
// Try to wait for loading ops on other threads if they happen to occur with a timeout.
|
||||
// This should be an edge, should it even occur.
|
||||
DateTime timeout = DateTime.Now.AddSeconds(2);
|
||||
while (timeout > DateTime.Now)
|
||||
{
|
||||
if (!AreOperationRunning)
|
||||
break;
|
||||
Thread.Sleep(1000/Timing.FixedUpdateRate-1);
|
||||
}
|
||||
|
||||
var wf = new WeakReference<IAssemblyLoaderService>(this);
|
||||
_onUnload?.Invoke(this);
|
||||
}
|
||||
|
||||
private void DisposeInternal()
|
||||
{
|
||||
IsDisposed = true;
|
||||
base.Resolving -= OnResolvingManagedAssembly;
|
||||
base.ResolvingUnmanagedDll -= OnResolvingUnmanagedDll;
|
||||
base.Unloading -= OnUnload;
|
||||
this._dependencyResolvers.Clear();
|
||||
this._loadedAssemblyData.Clear();
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var assembly))
|
||||
return assembly.Assembly;
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return 0;
|
||||
|
||||
GCHandle? handle = null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(unmanagedDllName, out var assemblyData))
|
||||
{
|
||||
handle = GCHandle.Alloc(assemblyData.Assembly, GCHandleType.Pinned);
|
||||
nint asmPtr = GCHandle.ToIntPtr(handle.Value);
|
||||
return asmPtr;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
try
|
||||
{
|
||||
if (handle.HasValue)
|
||||
handle.Value.Free();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored. We just want to ensure that free is called.
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private readonly record struct AssemblyData
|
||||
{
|
||||
public readonly Assembly Assembly;
|
||||
public readonly OneOf<byte[], string> AssemblyImageOrPath;
|
||||
public readonly MetadataReference AssemblyReference;
|
||||
public readonly ImmutableArray<Type> Types;
|
||||
public readonly ImmutableDictionary<string, Type> TypesByName;
|
||||
|
||||
public AssemblyData(Assembly assembly, byte[] assemblyImage)
|
||||
{
|
||||
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
|
||||
AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage));
|
||||
AssemblyReference = MetadataReference.CreateFromImage(assemblyImage);
|
||||
Types = assembly.GetSafeTypes().ToImmutableArray();
|
||||
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
|
||||
}
|
||||
|
||||
public AssemblyData(Assembly assembly, string path)
|
||||
{
|
||||
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
|
||||
AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path));
|
||||
AssemblyReference = MetadataReference.CreateFromFile(path);
|
||||
Types = assembly.GetSafeTypes().ToImmutableArray();
|
||||
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct AssemblyOrStringKey : IEquatable<AssemblyOrStringKey>, IEqualityComparer<AssemblyOrStringKey>
|
||||
{
|
||||
public Assembly Assembly { get; init; }
|
||||
public string AssemblyName { get; init; }
|
||||
public readonly int HashCode;
|
||||
|
||||
public AssemblyOrStringKey(Assembly assembly)
|
||||
{
|
||||
if(assembly == null)
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
Assembly = assembly;
|
||||
AssemblyName = assembly.GetName().FullName;
|
||||
if (AssemblyName == null)
|
||||
throw new ArgumentNullException(nameof(AssemblyName));
|
||||
HashCode = AssemblyName.GetHashCode();
|
||||
}
|
||||
|
||||
public AssemblyOrStringKey(string assemblyName)
|
||||
{
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
throw new ArgumentNullException(nameof(assemblyName));
|
||||
Assembly = null;
|
||||
AssemblyName = assemblyName;
|
||||
HashCode = AssemblyName.GetHashCode();
|
||||
}
|
||||
|
||||
public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y)
|
||||
{
|
||||
if (x.Assembly is not null && y.Assembly is not null)
|
||||
return x.Assembly == y.Assembly;
|
||||
return x.AssemblyName == y.AssemblyName;
|
||||
}
|
||||
|
||||
public int GetHashCode(AssemblyOrStringKey obj)
|
||||
{
|
||||
return this.HashCode;
|
||||
}
|
||||
|
||||
public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly);
|
||||
public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
using Barotrauma.LuaCs;
|
||||
using FluentResults;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IAssemblyLoaderService : IService
|
||||
{
|
||||
public interface IFactory : IService
|
||||
{
|
||||
IAssemblyLoaderService CreateInstance(LoaderInitData initData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor record for instancing.
|
||||
/// </summary>
|
||||
/// <param name="AssemblyManagementService"></param>
|
||||
/// <param name="InstanceId"></param>
|
||||
/// <param name="Name"></param>
|
||||
/// <param name="IsReferenceMode">Assemblies and Types in this context are for <see cref="MetadataReference"/> only.
|
||||
/// Execution of assembly data is forbidden.</param>
|
||||
/// <param name="OwnerPackage"></param>
|
||||
/// <param name="OnUnload"></param>
|
||||
/// <param name="OnResolvingManaged"></param>
|
||||
/// <param name="OnResolvingUnmanagedDll"></param>
|
||||
public record LoaderInitData(
|
||||
[Required] Guid InstanceId,
|
||||
[Required][NotNull] string Name,
|
||||
[Required] bool IsReferenceMode,
|
||||
ContentPackage OwnerPackage,
|
||||
Action<IAssemblyLoaderService> OnUnload,
|
||||
Func<IAssemblyLoaderService, AssemblyName, Assembly> OnResolvingManaged,
|
||||
Func<Assembly, string, IntPtr> OnResolvingUnmanagedDll);
|
||||
|
||||
/// <summary>
|
||||
/// ID for this instance.
|
||||
/// </summary>
|
||||
Guid Id { get; }
|
||||
/// <summary>
|
||||
/// The owner content package.
|
||||
/// </summary>
|
||||
ContentPackage OwnerPackage { get; }
|
||||
/// <summary>
|
||||
/// Indicates that the assemblies in this load context are metadata references only and not
|
||||
/// intended for execution.
|
||||
/// </summary>
|
||||
bool IsReferenceOnlyMode { get; }
|
||||
/// <summary>
|
||||
/// Runtime value of constant <see cref="InternalsAwareAssemblyName"/> for extensibility use.
|
||||
/// </summary>
|
||||
public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName;
|
||||
/// <summary>
|
||||
/// Name for all runtime-compiled assemblies requiring access to <c>internal</c> assembly components. <seealso cref="InternalsVisibleToAttribute"/>
|
||||
/// </summary>
|
||||
public const string InternalsAwareAssemblyName = "InternalsAwareAssembly";
|
||||
|
||||
/// <summary>
|
||||
/// Add additional locations for dependency resolution to use.
|
||||
/// </summary>
|
||||
/// <param name="paths"></param>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths);
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
|
||||
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
|
||||
/// AssemblyManager class.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName"><c>[NotNull]</c>Name reference of the assembly.
|
||||
/// <para><b>[IMPORTANT]</b> This is used to reference this assembly as the true name will be forced if
|
||||
/// publicized assemblies are not used (InternalsVisibleTo Attrib).</para>
|
||||
/// Must be supplied for in-memory assemblies.
|
||||
/// <para>Must be unique to all other assemblies explicitly loaded using this context.</para></param>
|
||||
/// <param name="compileWithInternalAccess">Forces the assembly name to <see cref="InternalsAccessAssemblyName"/> and grants access to <c>internal</c>.</param>
|
||||
/// <param name="syntaxTrees"><c>[NotNull]</c>Syntax trees to compile into the assembly.</param>
|
||||
/// <param name="metadataReferences">All <c>MetadataReference<c/>s to be used for compilation.
|
||||
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
|
||||
/// images not managed by the AssemblyManager class.</param>
|
||||
/// <param name="compilationOptions"><c>[NotNull]</c>CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
|
||||
/// <para><b>[IMPORTANT]</b>Cannot be null or empty if <see cref="compileWithInternalAccess"/> is false.</para></param>
|
||||
/// <returns>Success state of the operation.</returns>
|
||||
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
|
||||
bool compileWithInternalAccess,
|
||||
ImmutableArray<SyntaxTree> syntaxTrees,
|
||||
ImmutableArray<MetadataReference> metadataReferences,
|
||||
CSharpCompilationOptions compilationOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the assembly from the provided location and registers all new paths provided with dependency resolution.
|
||||
/// </summary>
|
||||
/// <param name="assemblyFilePath">Absolute path to the managed assembly.</param>
|
||||
/// <param name="additionalDependencyPaths">Additional paths for dependency resolution.</param>
|
||||
/// <returns>Success and reference to the assembly if successful.</returns>
|
||||
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
|
||||
ImmutableArray<string> additionalDependencyPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the already loaded assembly with the same name.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">Name of the assembly.</param>
|
||||
/// <returns>Operation success on assembly found and assembly.</returns>
|
||||
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <c>Type</c>s from loaded assemblies.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <c>Type</c>s from loaded assemblies. Does not create a defensive copy and blocks loading/unloading.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Type> UnsafeGetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first found type given it's fully qualified name.
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<Type> GetTypeInAssemblies(string typeName);
|
||||
|
||||
/// <summary>
|
||||
/// List of loaded assemblies.
|
||||
/// </summary>
|
||||
public IEnumerable<Assembly> Assemblies { get; }
|
||||
|
||||
public IEnumerable<MetadataReference> AssemblyReferences { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { }
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
[Serializable]
|
||||
[Obsolete($"Use {nameof(IModConfigInfo)} instead. This class exists for legacy compatibility only.")]
|
||||
public sealed class RunConfig : IRunConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// How should scripts be run on the server.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "Server")]
|
||||
[DefaultValue("Standard")]
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How should scripts be run on the client.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "Client")]
|
||||
[DefaultValue("Standard")]
|
||||
public string Client { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency").
|
||||
/// PIN Dependency checks if ContentPackage names contains the dependency string.
|
||||
/// </summary>
|
||||
[XmlArrayItem(ElementName = "Dependency", IsNullable = true, Type = typeof(Dependency))]
|
||||
[XmlArray]
|
||||
public Dependency[] Dependencies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the mod using non-publicized assemblies.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "UseNonPublicizedAssemblies")]
|
||||
public bool UseNonPublicizedAssemblies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the mod includes source files, the compiled assembly will be named "CompiledAssembly" and have the [InternalVisibleTo()] attribute applied to it.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "UseInternalAssemblyName")]
|
||||
[DefaultValue(false)]
|
||||
public bool UseInternalAssemblyName { get; set; }
|
||||
|
||||
[XmlElement(ElementName = "AutoGenerated")]
|
||||
public bool AutoGenerated { get; set; }
|
||||
|
||||
public RunConfig(bool autoGenerated)
|
||||
{
|
||||
this.AutoGenerated = autoGenerated;
|
||||
if (autoGenerated)
|
||||
{
|
||||
(Client, Server) = ("Standard", "Standard");
|
||||
UseNonPublicizedAssemblies = false;
|
||||
}
|
||||
}
|
||||
|
||||
public RunConfig() { } // For serialization use
|
||||
|
||||
[Serializable]
|
||||
public sealed class Dependency
|
||||
{
|
||||
/// <summary>
|
||||
/// Steam Workshop ID of the dependency.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "SteamWorkshopId")]
|
||||
public ulong SteamWorkshopId;
|
||||
|
||||
/// <summary>
|
||||
/// Package Name of the dependency. Not needed if SteamWorkshopId is set.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "PackageName")]
|
||||
public string PackageName;
|
||||
}
|
||||
|
||||
public RunConfig Sanitize()
|
||||
{
|
||||
try
|
||||
{
|
||||
Client = SanitizeRunSetting(Client);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Client = "Standard";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Server = SanitizeRunSetting(Server);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Server = "Standard";
|
||||
}
|
||||
|
||||
Dependencies ??= new RunConfig.Dependency[] { };
|
||||
|
||||
static string SanitizeRunSetting(string str) =>
|
||||
str switch
|
||||
{
|
||||
null => "Standard",
|
||||
"" => "Standard",
|
||||
" " => "Standard",
|
||||
_ => str[0].ToString().ToUpper() + str.Substring(1).ToLower()
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool IsForced()
|
||||
{
|
||||
#if CLIENT
|
||||
return this.Client == "Forced";
|
||||
#elif SERVER
|
||||
return this.Server == "Forced";
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool IsStandard()
|
||||
{
|
||||
#if CLIENT
|
||||
return this.Client == "Standard";
|
||||
#elif SERVER
|
||||
return this.Server == "Standard";
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool IsForcedOrStandard() => this.IsForced() || this.IsStandard();
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user