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.Text; using System.Threading; using Barotrauma.LuaCs; using Barotrauma.LuaCs.Services; using Barotrauma.Steam; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using MonoMod.Utils; // ReSharper disable InconsistentNaming 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;") .AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]") #if CLIENT .AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]") #elif SERVER .AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]") #endif .ToString(), ScriptParseOptions); private readonly string[] _publicizedAssembliesToLoad = { "BarotraumaCore.dll", #if CLIENT "Barotrauma.dll" #elif SERVER "DedicatedServer.dll" #endif }; private const string SCRIPT_FILE_REGEX = "*.cs"; private const string ASSEMBLY_FILE_REGEX = "*.dll"; private readonly float _assemblyUnloadTimeoutSeconds = 6f; private Guid _publicizedAssemblyLoader; private readonly List _currentPackagesByLoadOrder = new(); private readonly Dictionary> _packagesDependencies = new(); private readonly Dictionary _loadedCompiledPackageAssemblies = new(); private readonly Dictionary _reverseLookupGuidList = new(); private readonly Dictionary> _loadedPlugins = new (); private readonly Dictionary> _pluginTypes = new(); // where Type : IAssemblyPlugin private readonly Dictionary _packageRunConfigs = new(); private readonly Dictionary> _luaRegisteredTypes = new(); private readonly AssemblyManager _assemblyManager; private readonly LuaCsSetup _luaCsSetup; private DateTime _assemblyUnloadStartTime; #endregion #region PUBLIC_API #region LUA_EXTENSIONS /// /// 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. /// /// /// /// 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, _assemblyManager.TryGetSubTypesFromACL(guid, out var types) ? types.ToImmutableList() : ImmutableList.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 /// /// Whether assemblies have been loaded. /// public bool AssembliesLoaded { get; private set; } /// /// Whether loaded plugins had their preloader run. /// public bool PluginsPreInit { get; private set; } /// /// Whether plugins' types have been instantiated. /// public bool PluginsInitialized { get; private set; } /// /// Whether plugins are fully loaded. /// public bool PluginsLoaded { get; private set; } public IEnumerable GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder; /// /// Tries to find the content package that a given plugin belongs to. /// /// Package if found, null otherwise. /// The IAssemblyPlugin type to find. /// public bool TryGetPackageForPlugin(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; } /// /// Tries to get the loaded plugins for a given package. /// /// Package to find. /// The collection of loaded plugins. /// public bool TryGetLoadedPluginsForPackage(ContentPackage package, out IEnumerable 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; } /// /// Called when clean up is being performed. Use when relying on or making use of references from this manager. /// public event Action OnDispose; [MethodImpl(MethodImplOptions.Synchronized)] public void Dispose() { // send events for cleanup try { OnDispose?.Invoke(); } catch (Exception e) { ModUtils.Logging.PrintError($"Error while executing Dispose event: {e.Message}"); } // 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(); _publicizedAssemblyLoader = Guid.Empty; _packagesDependencies.Clear(); _loadedCompiledPackageAssemblies.Clear(); _reverseLookupGuidList.Clear(); _packageRunConfigs.Clear(); _currentPackagesByLoadOrder.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()) { Thread.Sleep(20); // give the assembly context unloader time to run (async) if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now) { break; } } _assemblyUnloadStartTime = DateTime.Now; Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies. while (!_assemblyManager.FinalizeDispose()) { Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies. if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now) { break; } } _assemblyManager.OnAssemblyLoaded -= AssemblyManagerOnAssemblyLoaded; _assemblyManager.OnAssemblyUnloading -= AssemblyManagerOnAssemblyUnloading; AssembliesLoaded = false; GC.SuppressFinalize(this); } /// /// Begins the loading process of scanning packages for scripts and binary assemblies, compiling and executing them. /// /// 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) _assemblyManager.FinalizeDispose(); //Update lists if (_assemblyManager.IsCurrentlyUnloading) { ModUtils.Logging.PrintMessage($"The below ACLs are still unloading:"); foreach (var wkref in _assemblyManager.StillUnloadingACLs) { if (wkref.TryGetTarget(out var tgt)) { ModUtils.Logging.PrintMessage($"ACL Name: {tgt.FriendlyName}"); foreach (Assembly assembly in tgt.Assemblies) { ModUtils.Logging.PrintMessage($"-- Assembly: {assembly.GetName()}"); } } } } ImmutableList publicizedAssemblies = ImmutableList.Empty; List publicizedAssembliesLocList = new(); foreach (string dllName in _publicizedAssembliesToLoad) { GetFiles(publicizedAssembliesLocList, dllName); } void GetFiles(List list, string searchQuery) { bool workshopFirst = _luaCsSetup.Config.PreferToUseWorkshopLuaSetup || LuaCsSetup.IsRunningInsideWorkshop; 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 (workshopFirst) { var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId); if (pck is not null) { publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized"); } } try { list.AddRange(Directory.GetFiles(publicizedDir, searchQuery)); } // no directory found, use the other one catch (DirectoryNotFoundException) { if (workshopFirst) { ModUtils.Logging.PrintError($"Unable to find /Binary/Publicized/ . Using Game folder instead."); publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized"); } else { ModUtils.Logging.PrintError($"Unable to find /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.AddRange(Directory.GetFiles(publicizedDir, searchQuery)); } } // try load them into an acl var loadState = _assemblyManager.LoadAssembliesFromLocations(publicizedAssembliesLocList, "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 packages = BuildPackagesList(); // check and load config _packageRunConfigs.AddRange(packages .Select(p => new KeyValuePair(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 cpNames = new(); HashSet 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 packagesToLoadInOrder = new(); // build load order if (reliableMap && OrderAndFilterPackagesByDependencies( _packagesDependencies, out var readyToLoad, out var cannotLoadPackages)) { 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( cp, new LoadableData( TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null, TryScanPackageForScripts(cp, out var list2) ? list2 : null, GetRunConfigForPackage(cp)))) .ToImmutableDictionary(); HashSet 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 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.Value.config.UseInternalAssemblyName ? "CompiledAssembly" : pair.Key.Name.Replace(" ",""), syntaxTrees, null, CompilationOptions, pair.Key.Name, ref id, pair.Value.config.UseNonPublicizedAssemblies ? null : 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(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) { throw new NotImplementedException(); } void UpdatePackagesToDisable(ref HashSet set, ContentPackage newDisabledPackage, IEnumerable>> dependenciesMap) { set.Add(newDisabledPackage); foreach (var package in dependenciesMap) { if (package.Value.Contains(newDisabledPackage)) set.Add(newDisabledPackage); } } } /// /// Executes instantiated plugins' Initialize() and OnLoadCompleted() methods. /// 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)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); } } foreach (var contentPlugins in _loadedPlugins) { // load complete foreach (var plugin in contentPlugins.Value) { TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); } } PluginsLoaded = true; } /// /// Executes instantiated plugins' PreInitPatching() method. /// 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)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); } } PluginsPreInit = true; } /// /// Initializes plugin types that are registered. /// /// 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); _loadedPlugins[pair.Key].Add(plugin); } catch (Exception e) { ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing..."); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); if (plugin is not null) { // ReSharper disable once AccessToModifiedClosure TryRun(() => plugin?.Dispose(), nameof(IAssemblyPlugin.Dispose), type.FullName ?? type.Name); plugin = null; } } } } PluginsInitialized = true; } /// /// Unloads all plugins by calling Dispose() on them. Note: This does not remove their external references nor /// unregister their types. /// public void UnloadPlugins() { foreach (var contentPlugins in _loadedPlugins) { foreach (var plugin in contentPlugins.Value) { TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}"); } contentPlugins.Value.Clear(); } _loadedPlugins.Clear(); PluginsInitialized = false; PluginsPreInit = false; PluginsLoaded = false; } /// /// Gets the RunConfig.xml for the given package located at [cp_root]/CSharp/RunConfig.xml. /// Generates a default config if one is not found. /// /// The package to search for. /// RunConfig data. /// True if a config is loaded, false if one was created. 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}"); ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}"); } } 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 scriptFilePaths) { string pathShared = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", "Shared"); string pathArch = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", ARCHITECTURE_TARGET); List 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.Empty; return false; } private static bool TryScanPackagesForAssemblies(ContentPackage package, out ImmutableList assemblyFilePaths) { string path = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "bin", ARCHITECTURE_TARGET, PLATFORM_TARGET); if (!Directory.Exists(path)) { assemblyFilePaths = ImmutableList.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 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; /// /// 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. /// /// List of packages to check /// Dependencies by package /// True if all dependencies were found. private static bool TryBuildDependenciesMap(ImmutableList packages, out Dictionary> 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.PrintWarning($"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; } } } } return reliableMap; } /// /// 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. /// /// A dictionary/map with key as the package and the elements as it's dependencies. /// List of packages that are ready to load and in the correct order. /// Packages with errors or cyclic dependencies. Element is error message. Null if empty. /// Optional: Allows for a custom checks to be performed on each package. /// Returns a bool indicating if the package is ready to load. /// Whether the process produces a usable list. private static bool OrderAndFilterPackagesByDependencies( Dictionary> packages, out IEnumerable readyToLoad, out IEnumerable> cannotLoadPackages, Func packageChecksPredicate = null) { HashSet completedPackages = new(); List readyPackages = new(); Dictionary unableToLoad = new(); HashSet currentNodeChain = new(); readyToLoad = readyPackages; try { foreach (var toProcessPack in packages) { ProcessPackage(toProcessPack.Key, toProcessPack.Value); } PackageProcRet ProcessPackage(ContentPackage packageToProcess, IEnumerable 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.PrintWarning( $"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 AssembliesFilePaths, ImmutableList ScriptsFilePaths, RunConfig config); #endregion } */