523ee8d641
- Added debug messages showing mods that have not unloaded from a previous session.
1089 lines
42 KiB
C#
1089 lines
42 KiB
C#
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;
|
|
|
|
// log error if some ACLs are still unloading (some assembly is still in use)
|
|
if (_assemblyManager.IsCurrentlyUnloading)
|
|
{
|
|
ModUtils.Logging.PrintError($"WARNING: Some mods from a previous session (lobby) are still loaded! This may result in undefined behaviour! Please restart your game. \nIf you wish to avoid this issue in the future, please disable the below mods and report the error to the mod author.");
|
|
foreach (var wkref in _assemblyManager.StillUnloadingACLs)
|
|
{
|
|
ModUtils.Logging.PrintError($"The below ACL is still unloading:");
|
|
if (wkref.TryGetTarget(out var tgt))
|
|
{
|
|
ModUtils.Logging.PrintError($"ACL Name: {tgt.Name}");
|
|
foreach (Assembly assembly in tgt.Assemblies)
|
|
{
|
|
ModUtils.Logging.PrintError($"-- Assembly: {assembly.GetName()}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// load publicized assemblies
|
|
var publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized");
|
|
|
|
// if using workshop lua setup is checked, try to use the publicized assemblies in the content package there instead.
|
|
if (_luaCsSetup.Config.PreferToUseWorkshopLuaSetup)
|
|
{
|
|
var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId);
|
|
if (pck is not null)
|
|
{
|
|
publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized");
|
|
}
|
|
}
|
|
|
|
ImmutableList<Assembly> publicizedAssemblies = ImmutableList<Assembly>.Empty;
|
|
ImmutableList<string> list;
|
|
|
|
try
|
|
{
|
|
// search for assemblies
|
|
list = Directory.GetFiles(publicizedDir, "*.dll")
|
|
#if CLIENT
|
|
.Where(s => !s.ToLowerInvariant().EndsWith("dedicatedserver.dll"))
|
|
#elif SERVER
|
|
.Where(s => !s.ToLowerInvariant().EndsWith("barotrauma.dll"))
|
|
#endif
|
|
.ToImmutableList();
|
|
|
|
if (list.Count < 1)
|
|
throw new DirectoryNotFoundException("No publicized assemblies found.");
|
|
}
|
|
// no directory found, use the other one
|
|
catch (DirectoryNotFoundException dne)
|
|
{
|
|
if (_luaCsSetup.Config.PreferToUseWorkshopLuaSetup)
|
|
{
|
|
ModUtils.Logging.PrintError($"Unable to find <LuaCsPackage>/Binary/Publicized/ . Using Game folder instead.");
|
|
publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized");
|
|
}
|
|
else
|
|
{
|
|
ModUtils.Logging.PrintError($"Unable to find <GameFolder>/Publicized/ . Using LuaCsPackage folder instead.");
|
|
var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId);
|
|
if (pck is not null)
|
|
{
|
|
publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized");
|
|
}
|
|
}
|
|
|
|
// search for assemblies
|
|
list = Directory.GetFiles(publicizedDir, "*.dll")
|
|
#if CLIENT
|
|
.Where(s => !s.ToLowerInvariant().EndsWith("dedicatedserver.dll"))
|
|
#elif SERVER
|
|
.Where(s => !s.ToLowerInvariant().EndsWith("barotrauma.dll"))
|
|
#endif
|
|
.ToImmutableList();
|
|
}
|
|
|
|
// try load them into an acl
|
|
var loadState = _assemblyManager.LoadAssembliesFromLocations(list, "luacs_publicized_assemblies", 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 cpToRunA = _packageRunConfigs
|
|
.Where(kvp => ShouldRunPackage(kvp.Key, kvp.Value))
|
|
.Select(kvp => kvp.Key)
|
|
.ToHashSet();
|
|
|
|
//-- filter and remove duplicate mods, prioritize /LocalMods/
|
|
HashSet<string> cpNames = new();
|
|
HashSet<string> duplicateNames = new();
|
|
|
|
// search
|
|
foreach (ContentPackage package in cpToRunA)
|
|
{
|
|
if (cpNames.Contains(package.Name))
|
|
{
|
|
if (!duplicateNames.Contains(package.Name))
|
|
{
|
|
duplicateNames.Add(package.Name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
cpNames.Add(package.Name);
|
|
}
|
|
}
|
|
|
|
// remove
|
|
foreach (string name in duplicateNames)
|
|
{
|
|
var duplCpList = cpToRunA
|
|
.Where(p => p.Name.Equals(name))
|
|
.ToHashSet();
|
|
|
|
if (duplCpList.Count < 2) // one or less found
|
|
continue;
|
|
|
|
ContentPackage toKeep = null;
|
|
foreach (ContentPackage package in duplCpList)
|
|
{
|
|
if (package.Dir.Contains("LocalMods"))
|
|
{
|
|
toKeep = package;
|
|
break;
|
|
}
|
|
}
|
|
|
|
toKeep ??= duplCpList.First();
|
|
|
|
duplCpList.Remove(toKeep); // remove all but this one
|
|
cpToRunA.RemoveWhere(p => duplCpList.Contains(p));
|
|
}
|
|
|
|
var cpToRun = cpToRunA.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, pair.Key.Name, 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,
|
|
pair.Key.Name, 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
|
|
}
|