Assembly and Script Loading Overhauled.

This commit is contained in:
MapleWheels
2023-09-17 01:04:51 -04:00
committed by Evil Factory
parent a58cb8251f
commit 414d46b33e
17 changed files with 2699 additions and 477 deletions
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Barotrauma
{
[Obsolete("Make your class implement IAssemblyPlugin instead.")]
public abstract class ACsMod : IAssemblyPlugin
{
private static List<ACsMod> mods = new List<ACsMod>();
public static List<ACsMod> LoadedMods { get => mods; }
private const string MOD_STORE = "LocalMods/.modstore";
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; }
/// Mod initialization
public ACsMod()
{
IsDisposed = false;
LoadedMods.Add(this);
}
/// <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)
{
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
LoadedMods.Remove(this);
IsDisposed = true;
}
public abstract void Stop();
}
}
@@ -0,0 +1,6 @@
namespace Barotrauma;
public enum ApplicationMode
{
Client, Server
}
@@ -0,0 +1,15 @@
namespace Barotrauma;
public enum AssemblyLoadingSuccessState
{
ACLLoadFailure,
AlreadyLoaded,
BadFilePath,
CannotLoadFile,
InvalidAssembly,
NoAssemblyFound,
PluginInstanceFailure,
BadName,
CannotLoadFromStream,
Success
}
@@ -0,0 +1,772 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable EventNeverSubscribedTo.Global
// ReSharper disable InconsistentNaming
namespace Barotrauma;
/***
* Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need
* becomes necessary as there is almost no serial performance overhead for adding threading protection.
*/
/// <summary>
/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin.
/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies.
/// </summary>
public partial class AssemblyManager
{
#region ExternalAPI
/// <summary>
/// Called when an assembly is loaded.
/// </summary>
public event Action<Assembly> OnAssemblyLoaded;
/// <summary>
/// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup
/// any references that you have to this assembly.
/// </summary>
public event Action<Assembly> OnAssemblyUnloading;
/// <summary>
/// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception.
/// </summary>
public event Action<string, Exception> OnException;
/// <summary>
/// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded.
/// </summary>
public event Action<Guid> OnACLUnload;
#if DEBUG
/// <summary>
/// [DEBUG ONLY]
/// Returns a list of the current unloading ACLs.
/// </summary>
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> StillUnloadingACLs
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.ToImmutableList();
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
#endif
// ReSharper disable once MemberCanBePrivate.Global
/// <summary>
/// Checks if there are any AssemblyLoadContexts still in the process of unloading.
/// </summary>
public bool IsCurrentlyUnloading
{
get
{
OpsLockUnloaded.EnterReadLock();
try
{
return UnloadingACLs.Any();
}
catch (Exception)
{
return false;
}
finally
{
OpsLockUnloaded.ExitReadLock();
}
}
}
// Old API compatibility
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>()
{
return GetSubTypesInLoadedAssemblies<T>(false);
}
/// <summary>
/// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom).
/// Warning: care should be used when using this method in hot paths as performance may be affected.
/// </summary>
/// <typeparam name="T">The type to compare against</typeparam>
/// <param name="rebuildList">Forces caches to clear and for the lists of types to be rebuilt.</param>
/// <returns>An Enumerator for matching types.</returns>
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>(bool rebuildList)
{
Type targetType = typeof(T);
string typeName = targetType.FullName ?? targetType.Name;
// rebuild
if (rebuildList)
RebuildTypesList();
// check cache
if (_subTypesLookupCache.TryGetValue(typeName, out var subTypeList))
{
return subTypeList;
}
// build from scratch
OpsLockLoaded.EnterReadLock();
try
{
// build list
var list1 = _defaultContextTypes
.Where(kvp1 => targetType.IsAssignableFrom(kvp1.Value) && !kvp1.Value.IsInterface)
.Concat(LoadedACLs
.SelectMany(kvp => kvp.Value.AssembliesTypes)
.Where(kvp2 => targetType.IsAssignableFrom(kvp2.Value) && !kvp2.Value.IsInterface))
.Select(kvp3 => kvp3.Value)
.ToImmutableList();
// only add if we find something
if (list1.Count > 0)
{
if (!_subTypesLookupCache.TryAdd(typeName, list1))
{
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!");
}
}
else
{
ModUtils.Logging.PrintMessage($"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}");
}
return list1;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Tries to get types assignable to type from the ACL given the Guid.
/// </summary>
/// <param name="id"></param>
/// <param name="types"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public bool TryGetSubTypesFromACL<T>(Guid id, out IEnumerable<Type> types)
{
Type targetType = typeof(T);
if (TryGetACL(id, out var acl))
{
types = acl.AssembliesTypes
.Where(kvp => targetType.IsAssignableFrom(kvp.Value) && !kvp.Value.IsInterface)
.Select(kvp => kvp.Value);
return true;
}
types = null;
return false;
}
/// <summary>
/// Tries to get types from the ACL given the Guid.
/// </summary>
/// <param name="id"></param>
/// <param name="types"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public bool TryGetSubTypesFromACL(Guid id, out IEnumerable<Type> types)
{
if (TryGetACL(id, out var acl))
{
types = acl.AssembliesTypes.Select(kvp => kvp.Value);
return true;
}
types = null;
return false;
}
/// <summary>
/// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string.
/// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ".
/// </summary>
/// <param name="name">The string name of the type to search for.</param>
/// <returns>An Enumerator for matching types.</returns>
public IEnumerable<Type> GetTypesByName(string typeName)
{
bool byRef = false;
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
{
typeName = typeName.Remove(0, 4);
byRef = true;
}
List<Type> types = new();
TypesListHelper();
if (types.Count > 0)
return types;
// we couldn't find it, rebuild and try one more time
RebuildTypesList();
TypesListHelper();
return types;
void TypesListHelper()
{
if (_defaultContextTypes.TryGetValue(typeName, out var type1))
{
if (type1 is not null)
types.Add(byRef ? type1.MakeByRefType() : type1);
}
OpsLockLoaded.EnterReadLock();
try
{
foreach (KeyValuePair<Guid,LoadedACL> loadedAcl in LoadedACLs)
{
var at = loadedAcl.Value.AssembliesTypes;
if (at.TryGetValue(typeName, out var type2))
{
if (type2 is not null)
types.Add(byRef ? type2.MakeByRefType() : type2);
}
}
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
}
/// <summary>
/// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr.
/// Warning: High usage may result in performance issues.
/// </summary>
/// <returns>An Enumerator for iteration.</returns>
public IEnumerable<Type> GetAllTypesInLoadedAssemblies()
{
OpsLockLoaded.EnterReadLock();
try
{
return AssemblyLoadContext.Default.Assemblies
.SelectMany(a => a.GetSafeTypes())
.Concat(LoadedACLs
.SelectMany(kvp => kvp.Value.AssembliesTypes.Select(kv => kv.Value)))
.ToImmutableList();
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Returns a list of all loaded ACLs.
/// WARNING: References to these ACLs outside of the AssemblyManager should be kept in a WeakReference in order
/// to avoid causing issues with unloading/disposal.
/// </summary>
/// <returns></returns>
public IEnumerable<LoadedACL> GetAllLoadedACLs()
{
try
{
OpsLockLoaded.EnterReadLock();
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
#endregion
#region InternalAPI
/// <summary>
/// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed.
/// </summary>
public event System.Func<LoadedACL, bool> IsReadyToUnloadACL;
public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName,
[NotNull] IEnumerable<SyntaxTree> syntaxTree,
IEnumerable<MetadataReference> externalMetadataReferences,
[NotNull] CSharpCompilationOptions compilationOptions,
ref Guid id,
IEnumerable<Assembly> externFileAssemblyRefs = null)
{
// validation
if (compiledAssemblyName.IsNullOrWhiteSpace())
return AssemblyLoadingSuccessState.BadName;
if (!GetOrCreateACL(id, out var acl))
return AssemblyLoadingSuccessState.ACLLoadFailure;
id = acl.Id; // pass on true id returned
// this acl is already hosting an in-memory assembly
if (acl.Acl.CompiledAssembly is not null)
return AssemblyLoadingSuccessState.AlreadyLoaded;
// compile
var state = acl.Acl.CompileAndLoadScriptAssembly(compiledAssemblyName, syntaxTree, externalMetadataReferences,
compilationOptions, out var messages, externFileAssemblyRefs);
// get types
if (state is AssemblyLoadingSuccessState.Success)
{
_subTypesLookupCache.Clear();
acl.RebuildTypesList();
OnAssemblyLoaded?.Invoke(acl.Acl.CompiledAssembly);
}
else
{
ModUtils.Logging.PrintError($"Unable to compile assembly '{compiledAssemblyName}' due to errors: {messages}");
}
return state;
}
/// <summary>
/// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it.
/// These ACLs are intended to be used to host Assemblies for information only and not for code execution.
/// WARNING: This process is irreversible.
/// </summary>
/// <param name="guid">Guid of the ACL.</param>
/// <returns>Whether or not an ACL was found with the given ID.</returns>
public bool SetACLToTemplateMode(Guid guid)
{
if (!TryGetACL(guid, out var acl))
return false;
acl.Acl.IsTemplateMode = true;
return true;
}
/// <summary>
/// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid.
/// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it.
/// </summary>
/// <param name="filePaths">List of assemblies to try and load.</param>
/// <param name="id">Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var.</param>
/// <returns>Operation success messages.</returns>
/// <exception cref="ArgumentNullException"></exception>
public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable<string> filePaths,
ref Guid id)
{
if (filePaths is null)
{
throw new ArgumentNullException(
$"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!");
}
ImmutableList<string> assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading
if (!assemblyFilePaths.Any())
{
return AssemblyLoadingSuccessState.NoAssemblyFound;
}
if (GetOrCreateACL(id, out var loadedAcl))
{
var state = loadedAcl.Acl.LoadFromFiles(assemblyFilePaths);
// if failure, we dispose of the acl
if (state != AssemblyLoadingSuccessState.Success)
{
DisposeACL(loadedAcl.Id);
ModUtils.Logging.PrintError($"ACL failed, unloading...");
return state;
}
// build types list
_subTypesLookupCache.Clear();
loadedAcl.RebuildTypesList();
id = loadedAcl.Id;
foreach (Assembly assembly in loadedAcl.Acl.Assemblies)
{
OnAssemblyLoaded?.Invoke(assembly);
}
return state;
}
return AssemblyLoadingSuccessState.ACLLoadFailure;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Synchronized)]
public bool TryBeginDispose()
{
OpsLockLoaded.EnterWriteLock();
OpsLockUnloaded.EnterWriteLock();
try
{
_subTypesLookupCache.Clear();
foreach (KeyValuePair<Guid, LoadedACL> loadedAcl in LoadedACLs)
{
if (loadedAcl.Value.Acl is not null)
{
foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList())
{
if (del is System.Func<LoadedACL, bool> { } func)
{
if (!func.Invoke(loadedAcl.Value))
return false; // Not ready, exit
}
}
foreach (Assembly assembly in loadedAcl.Value.Acl.Assemblies)
{
OnAssemblyUnloading?.Invoke(assembly);
}
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(loadedAcl.Value.Acl, true));
loadedAcl.Value.ClearTypesList();
loadedAcl.Value.Acl.Unload();
OnACLUnload?.Invoke(loadedAcl.Value.Id);
}
}
LoadedACLs.Clear();
return true;
}
catch
{
// should never happen
return false;
}
finally
{
OpsLockUnloaded.ExitWriteLock();
OpsLockLoaded.ExitWriteLock();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public bool FinalizeDispose()
{
bool isUnloaded;
OpsLockUnloaded.EnterUpgradeableReadLock();
try
{
List<WeakReference<MemoryFileAssemblyContextLoader>> toRemove = new();
foreach (WeakReference<MemoryFileAssemblyContextLoader> weakReference in UnloadingACLs)
{
if (!weakReference.TryGetTarget(out _))
{
toRemove.Add(weakReference);
}
}
if (toRemove.Any())
{
OpsLockUnloaded.EnterWriteLock();
try
{
foreach (WeakReference<MemoryFileAssemblyContextLoader> reference in toRemove)
{
UnloadingACLs.Remove(reference);
}
}
finally
{
OpsLockUnloaded.ExitWriteLock();
}
}
isUnloaded = !UnloadingACLs.Any();
}
finally
{
OpsLockUnloaded.ExitUpgradeableReadLock();
}
return isUnloaded;
}
/// <summary>
/// Tries to retrieve the LoadedACL with the given ID or null if none is found.
/// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference
/// to avoid causing unloading/disposal issues.
/// </summary>
/// <param name="id">GUID of the ACL.</param>
/// <param name="acl">The found ACL or null if none was found.</param>
/// <returns>Whether or not an ACL was found.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool TryGetACL(Guid id, out LoadedACL acl)
{
acl = null;
OpsLockLoaded.EnterReadLock();
try
{
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id))
return false;
acl = LoadedACLs[id];
return true;
}
finally
{
OpsLockLoaded.ExitReadLock();
}
}
/// <summary>
/// Gets or creates an AssemblyCtxLoader for the given ID. Creates if the ID is empty or no ACL can be found.
/// [IMPORTANT] After calling this method, the id you use should be taken from the acl container (acl.Id).
/// </summary>
/// <param name="id"></param>
/// <param name="acl"></param>
/// <returns>Should only return false if an error occurs.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private bool GetOrCreateACL(Guid id, out LoadedACL acl)
{
OpsLockLoaded.EnterUpgradeableReadLock();
try
{
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null)
{
OpsLockLoaded.EnterWriteLock();
try
{
id = Guid.NewGuid();
acl = new LoadedACL(id, this);
LoadedACLs[id] = acl;
return true;
}
finally
{
OpsLockLoaded.ExitWriteLock();
}
}
else
{
acl = LoadedACLs[id];
return true;
}
}
catch
{
// should never happen but in-case
acl = null;
return false;
}
finally
{
OpsLockLoaded.ExitUpgradeableReadLock();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private bool DisposeACL(Guid id)
{
OpsLockLoaded.EnterWriteLock();
OpsLockUnloaded.EnterWriteLock();
try
{
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null)
{
return false; // nothing to dispose of
}
var acl = LoadedACLs[id];
foreach (Assembly assembly in acl.Acl.Assemblies)
{
OnAssemblyUnloading?.Invoke(assembly);
}
_subTypesLookupCache.Clear();
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(acl.Acl, true));
acl.Acl.Unload();
OnACLUnload?.Invoke(acl.Id);
return true;
}
catch
{
// should never happen
return false;
}
finally
{
OpsLockLoaded.ExitWriteLock();
OpsLockUnloaded.ExitWriteLock();
}
}
internal AssemblyManager()
{
RebuildTypesList();
}
/// <summary>
/// Rebuilds the list of types in the default assembly load context.
/// </summary>
private void RebuildTypesList()
{
try
{
_defaultContextTypes = AssemblyLoadContext.Default.Assemblies
.SelectMany(a => a.GetSafeTypes())
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
_subTypesLookupCache.Clear();
}
catch(ArgumentException _)
{
try
{
// some types must've had duplicate type names, build the list while filtering
Dictionary<string, Type> types = new();
foreach (var type in AssemblyLoadContext.Default.Assemblies.SelectMany(a => a.GetSafeTypes()))
{
try
{
types.TryAdd(type.FullName ?? type.Name, type);
}
catch
{
// ignore, null key exception
}
}
_defaultContextTypes = types.ToImmutableDictionary();
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available.");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}");
#endif
_defaultContextTypes = ImmutableDictionary<string, Type>.Empty;
}
}
}
#endregion
#region Data
private readonly ConcurrentDictionary<string, ImmutableList<Type>> _subTypesLookupCache = new();
private ImmutableDictionary<string, Type> _defaultContextTypes;
private readonly ConcurrentDictionary<Guid, LoadedACL> LoadedACLs = new();
private readonly List<WeakReference<MemoryFileAssemblyContextLoader>> UnloadingACLs= new();
private readonly ReaderWriterLockSlim OpsLockLoaded = new ReaderWriterLockSlim();
private readonly ReaderWriterLockSlim OpsLockUnloaded = new ReaderWriterLockSlim();
#endregion
#region TypeDefs
public sealed class LoadedACL
{
public readonly Guid Id;
private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
public readonly MemoryFileAssemblyContextLoader Acl;
private readonly AssemblyManager _manager;
internal LoadedACL(Guid id, AssemblyManager manager)
{
this.Id = id;
this.Acl = new(manager);
this._manager = manager;
}
public ImmutableDictionary<string, Type> AssembliesTypes => _assembliesTypes;
/// <summary>
/// Rebuild the list of types from assemblies loaded in the AsmCtxLoader.
/// </summary>
internal void RebuildTypesList()
{
ClearTypesList();
try
{
_assembliesTypes = this.Acl.Assemblies
.SelectMany(a => a.GetSafeTypes())
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
}
catch(ArgumentException _)
{
// some types must've had duplicate type names, build the list while filtering
Dictionary<string, Type> types = new();
foreach (var type in this.Acl.Assemblies.SelectMany(a => a.GetSafeTypes()))
{
try
{
types.TryAdd(type.FullName ?? type.Name, type);
}
catch
{
// ignore, null key exception
}
}
_assembliesTypes = types.ToImmutableDictionary();
}
}
internal void ClearTypesList()
{
_assembliesTypes.Clear();
}
}
#endregion
}
public static class AssemblyExtensions
{
/// <summary>
/// Gets all types in the given assembly. Handles invalid type scenarios.
/// </summary>
/// <param name="assembly">The assembly to scan</param>
/// <returns>An enumerable collection of types.</returns>
public static IEnumerable<Type> GetSafeTypes(this Assembly assembly)
{
// Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException re)
{
try
{
return re.Types.Where(x => x != null)!;
}
catch (InvalidOperationException ioe)
{
return new List<Type>();
}
}
catch (Exception e)
{
return new List<Type>();
}
}
}
@@ -0,0 +1,978 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using Barotrauma.Steam;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using MonoMod.Utils;
namespace Barotrauma;
public sealed class CsPackageManager : IDisposable
{
#region PRIVATE_FUNCDATA
private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
.WithPreprocessorSymbols(new[]
{
#if SERVER
"SERVER"
#elif CLIENT
"CLIENT"
#else
"UNDEFINED"
#endif
#if DEBUG
,"DEBUG"
#endif
});
#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;")
#if CLIENT
.AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
#elif SERVER
.AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
#endif
.ToString(),
ScriptParseOptions);
private const string SCRIPT_FILE_REGEX = "*.cs";
private const string ASSEMBLY_FILE_REGEX = "*.dll";
private readonly float _assemblyUnloadTimeoutSeconds = 4f;
private Guid _publicizedAssemblyLoader;
private readonly List<ContentPackage> _currentPackagesByLoadOrder = new();
private readonly Dictionary<ContentPackage, ImmutableList<ContentPackage>> _packagesDependencies = new();
private readonly Dictionary<ContentPackage, Guid> _loadedCompiledPackageAssemblies = new();
private readonly Dictionary<Guid, ContentPackage> _reverseLookupGuidList = new();
private readonly Dictionary<Guid, HashSet<IAssemblyPlugin>> _loadedPlugins = new ();
private readonly Dictionary<Guid, ImmutableHashSet<Type>> _pluginTypes = new(); // where Type : IAssemblyPlugin
private readonly Dictionary<ContentPackage, RunConfig> _packageRunConfigs = new();
private readonly Dictionary<Guid, ImmutableList<Type>> _luaRegisteredTypes = new();
private readonly AssemblyManager _assemblyManager;
private readonly LuaCsSetup _luaCsSetup;
private DateTime _assemblyUnloadStartTime;
#endregion
#region PUBLIC_API
#region LUA_EXTENSIONS
/// <summary>
/// Searches for all types in all loaded assemblies from content packages who's names contain the name string and registers them with the Lua Interpreter.
/// </summary>
/// <param name="name"></param>
/// <param name="caseSensitive"></param>
/// <returns></returns>
public bool LuaTryRegisterPackageTypes(string name, bool caseSensitive = false)
{
if (!AssembliesLoaded)
return false;
var matchingPacks = _loadedCompiledPackageAssemblies
.Where(kvp => kvp.Key.Name.ToLowerInvariant().Contains(name.ToLowerInvariant()))
.Select(kvp => kvp.Value)
.ToImmutableList();
if (!matchingPacks.Any())
return false;
var types = matchingPacks
.Where(guid => !_luaRegisteredTypes.ContainsKey(guid))
.Select(guid => new KeyValuePair<Guid, ImmutableList<Type>>(
guid,
_assemblyManager.TryGetSubTypesFromACL(guid, out var types)
? types.ToImmutableList()
: ImmutableList<Type>.Empty))
.ToImmutableList();
if (!types.Any())
return false;
foreach (var kvp in types)
{
_luaRegisteredTypes[kvp.Key] = kvp.Value;
foreach (Type type in kvp.Value)
{
MoonSharp.Interpreter.UserData.RegisterType(type);
}
}
return true;
}
#endregion
/// <summary>
/// Whether or not assemblies have been loaded.
/// </summary>
public bool AssembliesLoaded { get; private set; }
/// <summary>
/// Whether or not loaded plugins had their preloader run.
/// </summary>
public bool PluginsPreInit { get; private set; }
/// <summary>
/// Whether or not plugins' types have been instantiated.
/// </summary>
public bool PluginsInitialized { get; private set; } = false;
/// <summary>
/// Whether or not plugins are fully loaded.
/// </summary>
public bool PluginsLoaded { get; private set; } = false;
public IEnumerable<ContentPackage> GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder;
/// <summary>
/// Tries to find the content package that a given plugin belongs to.
/// </summary>
/// <param name="package">Package if found, null otherwise.</param>
/// <typeparam name="T">The IAssemblyPlugin type to find.</typeparam>
/// <returns></returns>
public bool TryGetPackageForPlugin<T>(out ContentPackage package) where T : IAssemblyPlugin
{
package = null;
var t = typeof(T);
var guid = _pluginTypes
.Where(kvp => kvp.Value.Contains(t))
.Select(kvp => kvp.Key)
.FirstOrDefault(Guid.Empty);
if (guid.Equals(Guid.Empty) || !_reverseLookupGuidList.ContainsKey(guid) || _reverseLookupGuidList[guid] is null)
return false;
package = _reverseLookupGuidList[guid];
return true;
}
/// <summary>
/// Tries to get the loaded plugins for a given package.
/// </summary>
/// <param name="package">Package to find.</param>
/// <param name="loadedPlugins">The collection of loaded plugins.</param>
/// <returns></returns>
public bool TryGetLoadedPluginsForPackage(ContentPackage package, out IEnumerable<IAssemblyPlugin> loadedPlugins)
{
loadedPlugins = null;
if (package is null || !_loadedCompiledPackageAssemblies.ContainsKey(package))
return false;
var guid = _loadedCompiledPackageAssemblies[package];
if (guid.Equals(Guid.Empty) || !_loadedPlugins.ContainsKey(guid))
return false;
loadedPlugins = _loadedPlugins[guid];
return true;
}
/// <summary>
/// Called when clean up is being performed. Use when relying on or making use of references from this manager.
/// </summary>
public event Action OnDispose;
public void Dispose()
{
// send events for cleanup
OnDispose?.Invoke();
// cleanup events
if (OnDispose is not null)
{
foreach (Delegate del in OnDispose.GetInvocationList())
{
OnDispose -= (del as System.Action);
}
}
// cleanup plugins and assemblies
ReflectionUtils.ResetCache();
UnloadPlugins();
// try cleaning up the assemblies
_pluginTypes.Clear(); // remove assembly references
_loadedPlugins.Clear();
// lua cleanup
foreach (var kvp in _luaRegisteredTypes)
{
foreach (Type type in kvp.Value)
{
MoonSharp.Interpreter.UserData.UnregisterType(type);
}
}
_luaRegisteredTypes.Clear();
_assemblyUnloadStartTime = DateTime.Now;
_publicizedAssemblyLoader = Guid.Empty;
// we can't wait forever or app dies but we can try to be graceful
while (!_assemblyManager.TryBeginDispose())
{
if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
{
break;
}
}
_assemblyUnloadStartTime = DateTime.Now;
while (!_assemblyManager.FinalizeDispose())
{
if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
{
break;
}
}
_assemblyManager.OnAssemblyLoaded -= AssemblyManagerOnAssemblyLoaded;
_assemblyManager.OnAssemblyUnloading -= AssemblyManagerOnAssemblyUnloading;
_publicizedAssemblyLoader = Guid.Empty;
// clear lists after cleaning up
_packagesDependencies.Clear();
_loadedCompiledPackageAssemblies.Clear();
_reverseLookupGuidList.Clear();
_packageRunConfigs.Clear();
_currentPackagesByLoadOrder.Clear();
AssembliesLoaded = false;
GC.SuppressFinalize(this);
}
/// <summary>
/// Begins the loading process of scanning packages for scripts and binary assemblies, compiling and executing them.
/// </summary>
/// <returns></returns>
public AssemblyLoadingSuccessState LoadAssemblyPackages()
{
if (AssembliesLoaded)
{
return AssemblyLoadingSuccessState.AlreadyLoaded;
}
_assemblyManager.OnAssemblyLoaded += AssemblyManagerOnAssemblyLoaded;
_assemblyManager.OnAssemblyUnloading += AssemblyManagerOnAssemblyUnloading;
// load publicized assemblies
var publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized");
ImmutableList<Assembly> publicizedAssemblies = ImmutableList<Assembly>.Empty;
if (Directory.Exists(publicizedDir))
{
// search for assemblies
var list = Directory.GetFiles(publicizedDir, "*.dll")
#if CLIENT
.Where(s => !s.ToLowerInvariant().EndsWith("dedicatedserver.dll"));
#elif SERVER
.Where(s => !s.ToLowerInvariant().EndsWith("barotrauma.dll"));
#endif
// try load them into an acl
var loadState = _assemblyManager.LoadAssembliesFromLocations(list, ref _publicizedAssemblyLoader);
// loaded
if (loadState is AssemblyLoadingSuccessState.Success)
{
if (_assemblyManager.TryGetACL(_publicizedAssemblyLoader, out var acl))
{
publicizedAssemblies = acl.Acl.Assemblies.ToImmutableList();
_assemblyManager.SetACLToTemplateMode(_publicizedAssemblyLoader);
}
}
}
// get packages
IEnumerable<ContentPackage> packages = BuildPackagesList();
// check and load config
_packageRunConfigs.AddRange(packages
.Select(p => new KeyValuePair<ContentPackage, RunConfig>(p, GetRunConfigForPackage(p)))
.ToDictionary(p => p.Key, p=> p.Value));
// filter not to be loaded
var cpToRun = _packageRunConfigs
.Where(kvp => ShouldRunPackage(kvp.Key, kvp.Value))
.Select(kvp => kvp.Key)
.ToImmutableList();
// build dependencies map
bool reliableMap = TryBuildDependenciesMap(cpToRun, out var packDeps);
if (!reliableMap)
{
ModUtils.Logging.PrintMessage($"{nameof(CsPackageManager)}: Unable to create reliable dependencies map.");
}
_packagesDependencies.AddRange(packDeps.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToImmutableList())
);
List<ContentPackage> packagesToLoadInOrder = new();
// build load order
if (reliableMap && OrderAndFilterPackagesByDependencies(
_packagesDependencies,
out var readyToLoad,
out var cannotLoadPackages,
null))
{
packagesToLoadInOrder.AddRange(readyToLoad);
if (cannotLoadPackages is not null)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the following mods due to dependency errors:");
foreach (var pair in cannotLoadPackages)
{
ModUtils.Logging.PrintError($"Package: {pair.Key.Name} | Reason: {pair.Value}");
}
}
}
else
{
// use unsorted list on failure and send error message.
packagesToLoadInOrder.AddRange(_packagesDependencies.Select( p=> p.Key));
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to create a reliable load order. Defaulting to unordered loading!");
}
// get assemblies and scripts' filepaths from packages
var toLoad = packagesToLoadInOrder
.Select(cp => new KeyValuePair<ContentPackage, LoadableData>(
cp,
new LoadableData(
TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null,
TryScanPackageForScripts(cp, out var list2) ? list2 : null)))
.ToImmutableDictionary();
HashSet<ContentPackage> badPackages = new();
foreach (var pair in toLoad)
{
// check if unloadable
if (badPackages.Contains(pair.Key))
continue;
// try load binary assemblies
var id = Guid.Empty; // id for the ACL for this package defined by AssemblyManager.
AssemblyLoadingSuccessState successState;
if (pair.Value.AssembliesFilePaths is not null && pair.Value.AssembliesFilePaths.Any())
{
ModUtils.Logging.PrintMessage($"Loading assemblies for CPackage {pair.Key.Name}");
#if DEBUG
foreach (string assembliesFilePath in pair.Value.AssembliesFilePaths)
{
ModUtils.Logging.PrintMessage($"Found assemblies located at {Path.GetFullPath(ModUtils.IO.SanitizePath(assembliesFilePath))}");
}
#endif
successState = _assemblyManager.LoadAssembliesFromLocations(pair.Value.AssembliesFilePaths, ref id);
// error handling
if (successState is not AssemblyLoadingSuccessState.Success)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the binary assemblies for package {pair.Key.Name}. Error: {successState.ToString()}");
UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
continue;
}
}
// try compile scripts to assemblies
if (pair.Value.ScriptsFilePaths is not null && pair.Value.ScriptsFilePaths.Any())
{
ModUtils.Logging.PrintMessage($"Loading scripts for CPackage {pair.Key.Name}");
List<SyntaxTree> syntaxTrees = new();
syntaxTrees.Add(GetPackageScriptImports());
bool abortPackage = false;
// load scripts data from files
foreach (string scriptPath in pair.Value.ScriptsFilePaths)
{
var state = ModUtils.IO.GetOrCreateFileText(scriptPath, out string fileText, null, false);
// could not load file data
if (state is not ModUtils.IO.IOActionResultState.Success)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {state.ToString()}");
UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
abortPackage = true;
break;
}
try
{
CancellationToken token = new();
syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(fileText, ScriptParseOptions, scriptPath, Encoding.Default, token));
// cancel if parsing failed
if (token.IsCancellationRequested)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: Syntax Parse Error.");
UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
abortPackage = true;
break;
}
}
catch (Exception e)
{
// unknown error
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {e.Message}");
UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
abortPackage = true;
break;
}
}
if (abortPackage)
continue;
// try compile
successState = _assemblyManager.LoadAssemblyFromMemory(
pair.Key.Name.Replace(" ",""),
syntaxTrees,
null,
CompilationOptions,
ref id, publicizedAssemblies);
if (successState is not AssemblyLoadingSuccessState.Success)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to compile script assembly for package {pair.Key.Name}. Error: {successState.ToString()}");
UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
continue;
}
}
// something was loaded, add to index
if (id != Guid.Empty)
{
ModUtils.Logging.PrintMessage($"Assemblies from CPackage {pair.Key.Name} loaded with Guid {id}.");
_loadedCompiledPackageAssemblies.Add(pair.Key, id);
_reverseLookupGuidList.Add(id, pair.Key);
}
}
// update loaded packages to exclude bad packages
_currentPackagesByLoadOrder.AddRange(toLoad
.Where(p => !badPackages.Contains(p.Key))
.Select(p => p.Key));
// build list of plugins
foreach (var pair in _loadedCompiledPackageAssemblies)
{
if (_assemblyManager.TryGetSubTypesFromACL<IAssemblyPlugin>(pair.Value, out var types))
{
_pluginTypes[pair.Value] = types.ToImmutableHashSet();
foreach (var type in _pluginTypes[pair.Value])
{
ModUtils.Logging.PrintMessage($"Loading type: {type.Name}");
}
}
}
this.AssembliesLoaded = true;
return AssemblyLoadingSuccessState.Success;
bool ShouldRunPackage(ContentPackage package, RunConfig config)
{
if (config.AutoGenerated)
return false;
return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced())
|| (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard());
}
void UpdatePackagesToDisable(ref HashSet<ContentPackage> list,
ContentPackage newDisabledPackage,
IEnumerable<KeyValuePair<ContentPackage, ImmutableList<ContentPackage>>> dependenciesMap)
{
list.Add(newDisabledPackage);
foreach (var package in dependenciesMap)
{
if (package.Value.Contains(newDisabledPackage))
list.Add(newDisabledPackage);
}
}
}
/// <summary>
/// Executes instantiated plugins' Initialize() and OnLoadCompleted() methods.
/// </summary>
public void RunPluginsInit()
{
if (!AssembliesLoaded)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without any loaded assemblies!");
return;
}
if (!PluginsInitialized)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without type instantiation!");
return;
}
if (PluginsLoaded)
return;
foreach (var contentPlugins in _loadedPlugins)
{
// init
foreach (var plugin in contentPlugins.Value)
{
TryRun(() => plugin.Initialize(), $"{nameof(IAssemblyPlugin.Initialize)}", plugin.GetType().Name);
}
}
foreach (var contentPlugins in _loadedPlugins)
{
// load complete
foreach (var plugin in contentPlugins.Value)
{
TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", plugin.GetType().Name);
}
}
PluginsLoaded = true;
}
/// <summary>
/// Executes instantiated plugins' PreInitPatching() method.
/// </summary>
public void RunPluginsPreInit()
{
if (!AssembliesLoaded)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without any loaded assemblies!");
return;
}
if (!PluginsInitialized)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without type initialization!");
return;
}
if (PluginsPreInit)
{
return;
}
foreach (var contentPlugins in _loadedPlugins)
{
// init
foreach (var plugin in contentPlugins.Value)
{
TryRun(() => plugin.PreInitPatching(), $"{nameof(IAssemblyPlugin.PreInitPatching)}", plugin.GetType().Name);
}
}
PluginsPreInit = true;
}
/// <summary>
/// Initializes plugin types that are registered.
/// </summary>
/// <param name="force"></param>
public void InstantiatePlugins(bool force = false)
{
if (!AssembliesLoaded)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to instantiate plugins without any loaded assemblies!");
return;
}
if (PluginsInitialized)
{
if (force)
UnloadPlugins();
else
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to load plugins when they were already loaded!");
return;
}
}
foreach (var pair in _pluginTypes)
{
// instantiate
foreach (Type type in pair.Value)
{
if (!_loadedPlugins.ContainsKey(pair.Key))
_loadedPlugins.Add(pair.Key, new());
else if (_loadedPlugins[pair.Key] is null)
_loadedPlugins[pair.Key] = new();
IAssemblyPlugin plugin = null;
try
{
plugin = (IAssemblyPlugin)Activator.CreateInstance(type);
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing...");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
#endif
TryRun(() => plugin?.Dispose(), "Dispose", type.FullName ?? type.Name);
plugin = null;
}
if (plugin is not null)
_loadedPlugins[pair.Key].Add(plugin);
else
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}");
}
}
PluginsInitialized = true;
}
/// <summary>
/// Unloads all plugins by calling Dispose() on them. Note: This does not remove their external references nor
/// unregister their types.
/// </summary>
public void UnloadPlugins()
{
foreach (var contentPlugins in _loadedPlugins)
{
foreach (var plugin in contentPlugins.Value)
{
TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", plugin.GetType().Name);
}
contentPlugins.Value.Clear();
}
_loadedPlugins.Clear();
PluginsInitialized = false;
PluginsPreInit = false;
PluginsLoaded = false;
}
/// <summary>
/// Gets the RunConfig.xml for the given package located at [cp_root]/CSharp/RunConfig.xml.
/// Generates a default config if one is not found.
/// </summary>
/// <param name="package">The package to search for.</param>
/// <param name="config">RunConfig data.</param>
/// <returns>True if a config is loaded, false if one was created.</returns>
public static bool GetOrCreateRunConfig(ContentPackage package, out RunConfig config)
{
var path = System.IO.Path.Combine(Path.GetFullPath(package.Dir), "CSharp", "RunConfig.xml");
if (!File.Exists(path))
{
config = new RunConfig(true).Sanitize();
return false;
}
return ModUtils.IO.LoadOrCreateTypeXml(out config, path, () => new RunConfig(true).Sanitize(), false);
}
#endregion
#region INTERNALS
private void TryRun(Action action, string messageMethodName, string messageTypeName)
{
try
{
action?.Invoke();
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while running {messageMethodName}() on plugin of type {messageTypeName}");
#if DEBUG
ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
#endif
}
}
private void AssemblyManagerOnAssemblyUnloading(Assembly assembly)
{
ReflectionUtils.RemoveAssemblyFromCache(assembly);
}
private void AssemblyManagerOnAssemblyLoaded(Assembly assembly)
{
//ReflectionUtils.AddNonAbstractAssemblyTypes(assembly);
// As ReflectionUtils.GetDerivedNonAbstract is only used for Prefabs & Barotrauma-specific implementing types,
// we can safely not register System/Core assemblies.
if (assembly.FullName is not null && assembly.FullName.StartsWith("System."))
return;
ReflectionUtils.AddNonAbstractAssemblyTypes(assembly, true);
}
internal CsPackageManager([NotNull] AssemblyManager assemblyManager, [NotNull] LuaCsSetup luaCsSetup)
{
this._assemblyManager = assemblyManager;
this._luaCsSetup = luaCsSetup;
}
~CsPackageManager()
{
this.Dispose();
}
private static bool TryScanPackageForScripts(ContentPackage package, out ImmutableList<string> scriptFilePaths)
{
string pathShared = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", "Shared");
string pathArch = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", ARCHITECTURE_TARGET);
List<string> files = new();
if (Directory.Exists(pathShared))
files.AddRange(Directory.GetFiles(pathShared, SCRIPT_FILE_REGEX, SearchOption.AllDirectories));
if (Directory.Exists(pathArch))
files.AddRange(Directory.GetFiles(pathArch, SCRIPT_FILE_REGEX, SearchOption.AllDirectories));
if (files.Count > 0)
{
scriptFilePaths = files.ToImmutableList();
return true;
}
scriptFilePaths = ImmutableList<string>.Empty;
return false;
}
private static bool TryScanPackagesForAssemblies(ContentPackage package, out ImmutableList<string> assemblyFilePaths)
{
string path = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "bin", ARCHITECTURE_TARGET, PLATFORM_TARGET);
if (!Directory.Exists(path))
{
assemblyFilePaths = ImmutableList<string>.Empty;
return false;
}
assemblyFilePaths = System.IO.Directory.GetFiles(path, ASSEMBLY_FILE_REGEX, SearchOption.AllDirectories)
.ToImmutableList();
return assemblyFilePaths.Count > 0;
}
private static RunConfig GetRunConfigForPackage(ContentPackage package)
{
if (!GetOrCreateRunConfig(package, out var config))
config.AutoGenerated = true;
return config;
}
private IEnumerable<ContentPackage> BuildPackagesList()
{
// get unique list of content packages.
// Note: there is an old issue where the AllPackages group
// would sometimes not contain packages downloaded from the host, so we union enabled.
return ContentPackageManager.AllPackages.Union(ContentPackageManager.EnabledPackages.All).Where(pack => !pack.Name.ToLowerInvariant().Equals("vanilla"));
}
private static SyntaxTree GetPackageScriptImports() => BaseAssemblyImports;
/// <summary>
/// Builds a list of ContentPackage dependencies for each of the packages in the list. Note: All dependencies must be included in the provided list of packages.
/// </summary>
/// <param name="packages">List of packages to check</param>
/// <param name="dependenciesMap">Dependencies by package</param>
/// <returns>True if all dependencies were found.</returns>
private static bool TryBuildDependenciesMap(ImmutableList<ContentPackage> packages, out Dictionary<ContentPackage, List<ContentPackage>> dependenciesMap)
{
bool reliableMap = true; // remains true if all deps were found.
dependenciesMap = new();
foreach (var package in packages)
{
dependenciesMap.Add(package, new());
if (GetOrCreateRunConfig(package, out var config))
{
if (config.Dependencies is null || !config.Dependencies.Any())
continue;
foreach (RunConfig.Dependency dependency in config.Dependencies)
{
ContentPackage dep = packages.FirstOrDefault(p =>
(dependency.SteamWorkshopId != 0 && p.TryExtractSteamWorkshopId(out var steamWorkshopId)
&& steamWorkshopId.Value == dependency.SteamWorkshopId)
|| (!dependency.PackageName.IsNullOrWhiteSpace() && p.Name.ToLowerInvariant().Contains(dependency.PackageName.ToLowerInvariant())), null);
if (dep is not null)
{
dependenciesMap[package].Add(dep);
}
else
{
ModUtils.Logging.PrintError($"Warning! The ContentPackage {package.Name} lists a dependency of (STEAMID: {dependency.SteamWorkshopId}, PackageName: {dependency.PackageName}) but it could not be found in the to-be-loaded CSharp packages list!");
reliableMap = false;
}
}
}
else
{
ModUtils.Logging.PrintMessage($"Warning! Could not retrieve RunConfig for ContentPackage {package.Name}!");
}
}
return reliableMap;
}
/// <summary>
/// Given a table of packages and dependent packages, will sort them by dependency loading order along with packages
/// that cannot be loaded due to errors or failing the predicate checks.
/// </summary>
/// <param name="packages">A dictionary/map with key as the package and the elements as it's dependencies.</param>
/// <param name="readyToLoad">List of packages that are ready to load and in the correct order.</param>
/// <param name="cannotLoadPackages">Packages with errors or cyclic dependencies. Element is error message. Null if empty.</param>
/// <param name="packageChecksPredicate">Optional: Allows for a custom checks to be performed on each package.
/// Returns a bool indicating if the package is ready to load.</param>
/// <returns>Whether or not the process produces a usable list.</returns>
private static bool OrderAndFilterPackagesByDependencies(
Dictionary<ContentPackage, ImmutableList<ContentPackage>> packages,
out IEnumerable<ContentPackage> readyToLoad,
out IEnumerable<KeyValuePair<ContentPackage, string>> cannotLoadPackages,
Func<ContentPackage, bool> packageChecksPredicate = null)
{
HashSet<ContentPackage> completedPackages = new();
List<ContentPackage> readyPackages = new();
Dictionary<ContentPackage, string> unableToLoad = new();
HashSet<ContentPackage> currentNodeChain = new();
readyToLoad = readyPackages;
try
{
foreach (var toProcessPack in packages)
{
ProcessPackage(toProcessPack.Key, toProcessPack.Value);
}
PackageProcRet ProcessPackage(ContentPackage packageToProcess, IEnumerable<ContentPackage> dependencies)
{
//cyclic handling
if (unableToLoad.ContainsKey(packageToProcess))
{
return PackageProcRet.BadPackage;
}
// already processed
if (completedPackages.Contains(packageToProcess))
{
return PackageProcRet.AlreadyCompleted;
}
// cyclic check
if (currentNodeChain.Contains(packageToProcess))
{
StringBuilder sb = new();
sb.AppendLine("Error: Cyclic Dependency. ")
.Append(
"The following ContentPackages rely on eachother in a way that makes it impossible to know which to load first! ")
.Append(
"Note: the package listed twice shows where the cycle starts/ends and is not necessarily the problematic package.");
int i = 0;
foreach (var package in currentNodeChain)
{
i++;
sb.AppendLine($"{i}. {package.Name}");
}
sb.AppendLine($"{i}. {packageToProcess.Name}");
unableToLoad.Add(packageToProcess, sb.ToString());
completedPackages.Add(packageToProcess);
return PackageProcRet.BadPackage;
}
if (packageChecksPredicate is not null && !packageChecksPredicate.Invoke(packageToProcess))
{
unableToLoad.Add(packageToProcess, $"Unable to load package {packageToProcess.Name} due to failing checks.");
completedPackages.Add(packageToProcess);
return PackageProcRet.BadPackage;
}
currentNodeChain.Add(packageToProcess);
foreach (ContentPackage dependency in dependencies)
{
// The mod lists a dependent that was not found during the discovery phase.
if (!packages.ContainsKey(dependency))
{
// search to see if it's enabled
if (!ContentPackageManager.EnabledPackages.All.Contains(dependency))
{
// present warning but allow loading anyways, better to let the user just disable the package if it's really an issue.
ModUtils.Logging.PrintError(
$"Warning: the ContentPackage of {packageToProcess.Name} requires the Dependency {dependency.Name} but this package wasn't found in the enabled mods list!");
}
continue;
}
var ret = ProcessPackage(dependency, packages[dependency]);
if (ret is PackageProcRet.BadPackage)
{
if (!unableToLoad.ContainsKey(packageToProcess))
{
unableToLoad.Add(packageToProcess, $"Error: Dependency failure. Failed to load {dependency.Name}");
}
currentNodeChain.Remove(packageToProcess);
if (!completedPackages.Contains(packageToProcess))
{
completedPackages.Add(packageToProcess);
}
return PackageProcRet.BadPackage;
}
}
currentNodeChain.Remove(packageToProcess);
completedPackages.Add(packageToProcess);
readyPackages.Add(packageToProcess);
return PackageProcRet.Completed;
}
}
catch (Exception e)
{
ModUtils.Logging.PrintError($"Error while generating dependency loading order! Exception: {e.Message}");
#if DEBUG
ModUtils.Logging.PrintError($"Stack Trace: {e.StackTrace}");
#endif
cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null;
return false;
}
cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null;
return true;
}
private enum PackageProcRet : byte
{
AlreadyCompleted,
Completed,
BadPackage
}
private record LoadableData(ImmutableList<string> AssembliesFilePaths, ImmutableList<string> ScriptsFilePaths);
#endregion
}
@@ -0,0 +1,22 @@
using System;
namespace Barotrauma;
public interface IAssemblyPlugin : IDisposable
{
/// <summary>
/// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content.
/// </summary>
void Initialize();
/// <summary>
/// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here.
/// </summary>
void OnLoadCompleted();
/// <summary>
/// Called before Barotrauma initializes vanilla content. WARNING: This method may be called before Initialize()!
/// </summary>
void PreInitPatching();
}
@@ -0,0 +1,289 @@
using System;
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.Loader;
using System.Threading;
using Barotrauma;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
namespace Barotrauma;
/// <summary>
/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution.
/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more.
/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first.
/// </summary>
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
{
// public
// ReSharper disable MemberCanBePrivate.Global
public Assembly CompiledAssembly { get; private set; } = null;
public byte[] CompiledAssemblyImage { get; private set; } = null;
// ReSharper restore MemberCanBePrivate.Global
// internal
private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
protected bool IsResolving; //this is to avoid circular dependency lookup.
private AssemblyManager _assemblyManager;
public bool IsTemplateMode { get; set; } = false;
public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
{
this._assemblyManager = assemblyManager;
}
/// <summary>
/// Try to load the list of disk-file assemblies.
/// </summary>
/// <param name="assemblyFilePaths">Operation success or failure reason.</param>
public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
{
if (assemblyFilePaths is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
foreach (string filepath in assemblyFilePaths)
{
// path verification
if (filepath.IsNullOrWhiteSpace())
continue;
string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);
if (directoryKey is null)
return AssemblyLoadingSuccessState.BadFilePath;
// setup dep resolver if not available
if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
{
_dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
}
// try loading the assemblies
try
{
LoadFromAssemblyPath(sanitizedFilePath);
}
// on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
catch (ArgumentNullException ane)
{
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (ArgumentException ae)
{
return AssemblyLoadingSuccessState.BadFilePath;
}
catch (FileLoadException fle)
{
return AssemblyLoadingSuccessState.CannotLoadFile;
}
catch (FileNotFoundException fne)
{
return AssemblyLoadingSuccessState.NoAssemblyFound;
}
catch (BadImageFormatException bfe)
{
return AssemblyLoadingSuccessState.InvalidAssembly;
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.ACLLoadFailure;
}
}
return AssemblyLoadingSuccessState.Success;
}
/// <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">Name of the assembly. Must be supplied for in-memory assemblies.</param>
/// <param name="syntaxTrees">Syntax trees to compile into the assembly.</param>
/// <param name="externMetadataReferences">Metadata 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">CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
/// <param name="compilationMessages">Will contain any diagnostic messages for compilation failure.</param>
/// <param name="externFileAssemblyReferences">Additional assemblies located in the FileSystem to build metadata references from.
/// Assemblies here will have duplicates by the same name that are currently loaded filtered out.</param>
/// <returns>Success state of the operation.</returns>
/// <exception cref="ArgumentNullException">Throws exception if any of the required arguments are null.</exception>
public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
[NotNull] string assemblyName,
[NotNull] IEnumerable<SyntaxTree> syntaxTrees,
IEnumerable<MetadataReference> externMetadataReferences,
[NotNull] CSharpCompilationOptions compilationOptions,
out string compilationMessages,
IEnumerable<Assembly> externFileAssemblyReferences = null)
{
compilationMessages = "";
if (this.CompiledAssembly is not null)
{
return AssemblyLoadingSuccessState.AlreadyLoaded;
}
var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
.Where(a => a.FullName is not null)
.Select(a => a.FullName).ToImmutableHashSet()
: ImmutableHashSet<string>.Empty;
// verifications
if (assemblyName.IsNullOrWhiteSpace())
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");
if (syntaxTrees is null)
throw new ArgumentNullException(
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
// add external references
List<MetadataReference> metadataReferences = new();
if (externMetadataReferences is not null)
metadataReferences.AddRange(externMetadataReferences);
// build metadata refs from global where not an in-memory compiled assembly and not the same assembly as supplied.
metadataReferences.AddRange(AppDomain.CurrentDomain.GetAssemblies()
.Where(a =>
{
if (a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit"))
return false;
if (a.FullName is null)
return true;
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
})
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
.Union(externAssemblyRefs // add custom supplied assemblies
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
).ToList());
// build metadata refs from in-memory images
foreach (var loadedAcl in _assemblyManager.GetAllLoadedACLs())
{
if (loadedAcl.Acl.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
continue;
metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
}
// Change inaccessible options to allow public access to restricted members
var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
// begin compilation
using var memoryCompilation = new MemoryStream();
// compile, emit
var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
// check for errors
if (!result.Success)
{
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
foreach (Diagnostic diagnostic in failures)
{
compilationMessages += $"\n{diagnostic}";
}
return AssemblyLoadingSuccessState.InvalidAssembly;
}
// read compiled assembly from memory stream into an in-memory assembly & image
memoryCompilation.Seek(0, SeekOrigin.Begin); // reset
try
{
CompiledAssembly = LoadFromStream(memoryCompilation);
CompiledAssemblyImage = memoryCompilation.ToArray();
}
catch (Exception e)
{
#if SERVER
LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#elif CLIENT
LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
#endif
return AssemblyLoadingSuccessState.CannotLoadFromStream;
}
return AssemblyLoadingSuccessState.Success;
}
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
protected override Assembly Load(AssemblyName assemblyName)
{
if (IsResolving)
return null; //circular resolution fast exit.
try
{
IsResolving = true;
// resolve self collection
Assembly ass = this.Assemblies.FirstOrDefault(a =>
a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
if (ass is not null)
return ass;
// resolve to local folders
foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
{
var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
if (asspath is null)
continue;
ass = LoadFromAssemblyPath(asspath);
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (ass is not null)
return ass;
}
//try resolve against other loaded alcs
foreach (var loadedAcL in _assemblyManager.GetAllLoadedACLs())
{
if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode) continue;
try
{
ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
catch
{
// LoadFromAssemblyName throws, no need to propagate
}
}
ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
if (ass is not null)
return ass;
}
finally
{
IsResolving = false;
}
return null;
}
private new void Unload()
{
CompiledAssembly = null;
CompiledAssemblyImage = null;
base.Unload();
}
}
@@ -0,0 +1,111 @@
using System;
using System.Xml.Serialization;
namespace Barotrauma;
[Serializable]
public sealed class RunConfig
{
/// <summary>
/// How should scripts be run on the server.
/// </summary>
[XmlElement(ElementName = "Server")] public string Server;
/// <summary>
/// How should scripts be run on the client.
/// </summary>
[XmlElement(ElementName = "Client")] public string Client;
/// <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; }
[XmlElement(ElementName = "AutoGenerated")]
public bool AutoGenerated { get; set; }
public RunConfig(bool autoGenerated)
{
this.AutoGenerated = autoGenerated;
if (autoGenerated)
{
(Client, Server) = ("None", "None");
}
}
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 e)
{
Client = "None";
}
try
{
Server = SanitizeRunSetting(Server);
}
catch (Exception e)
{
Server = "None";
}
Dependencies ??= new RunConfig.Dependency[] { };
static string SanitizeRunSetting(string str) =>
str switch
{
null => "None",
"" => "None",
" " => "None",
_ => str[0].ToString().ToUpper() + str.Substring(1).ToLower()
};
return this;
}
public bool IsForced()
{
#if CLIENT
return this.Client.Equals("Forced");
#elif SERVER
return this.Server.Equals("Forced");
#endif
}
public bool IsStandard()
{
#if CLIENT
return this.Client.Equals("Standard");
#elif SERVER
return this.Server.Equals("Standard");
#endif
}
public bool IsForcedOrStandard() => this.IsForced() || this.IsStandard();
}