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 _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 or not assemblies have been loaded. /// public bool AssembliesLoaded { get; private set; } /// /// Whether or not loaded plugins had their preloader run. /// public bool PluginsPreInit { get; private set; } /// /// Whether or not plugins' types have been instantiated. /// public bool PluginsInitialized { get; private set; } = false; /// /// Whether or not plugins are fully loaded. /// public bool PluginsLoaded { get; private set; } = false; 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; 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); } /// /// 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) 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 publicizedAssemblies = ImmutableList.Empty; ImmutableList 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 /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 = 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 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, 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( cp, new LoadableData( TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null, TryScanPackageForScripts(cp, out var list2) ? list2 : null))) .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.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(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 list, ContentPackage newDisabledPackage, IEnumerable>> dependenciesMap) { list.Add(newDisabledPackage); foreach (var package in dependenciesMap) { if (package.Value.Contains(newDisabledPackage)) list.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)}", 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; } /// /// 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)}", 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); } 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; } /// /// 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)}", 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}"); #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 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.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; } /// /// 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 or not 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.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 AssembliesFilePaths, ImmutableList ScriptsFilePaths); #endregion }