Client LuaCsForBarotrauma
CsPackageManager.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using System.Diagnostics.CodeAnalysis;
5 using System.IO;
6 using System.Linq;
7 using System.Reflection;
8 using System.Runtime.CompilerServices;
9 using System.Text;
10 using System.Threading;
11 using Barotrauma.Steam;
12 using Microsoft.CodeAnalysis;
13 using Microsoft.CodeAnalysis.CSharp;
14 using MonoMod.Utils;
15 
16 // ReSharper disable InconsistentNaming
17 
18 namespace Barotrauma;
19 
20 public sealed class CsPackageManager : IDisposable
21 {
22  #region PRIVATE_FUNCDATA
23 
24  private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
25  .WithPreprocessorSymbols(new[]
26  {
27 #if SERVER
28  "SERVER"
29 #elif CLIENT
30  "CLIENT"
31 #else
32  "UNDEFINED"
33 #endif
34 #if DEBUG
35  ,"DEBUG"
36 #endif
37  });
38 
39 #if WINDOWS
40  private const string PLATFORM_TARGET = "Windows";
41 #elif OSX
42  private const string PLATFORM_TARGET = "OSX";
43 #elif LINUX
44  private const string PLATFORM_TARGET = "Linux";
45 #endif
46 
47 #if CLIENT
48  private const string ARCHITECTURE_TARGET = "Client";
49 #elif SERVER
50  private const string ARCHITECTURE_TARGET = "Server";
51 #endif
52 
53  private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
54  .WithMetadataImportOptions(MetadataImportOptions.All)
55 #if DEBUG
56  .WithOptimizationLevel(OptimizationLevel.Debug)
57 #else
58  .WithOptimizationLevel(OptimizationLevel.Release)
59 #endif
60  .WithAllowUnsafe(true);
61 
62  private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText(
63  new StringBuilder()
64  .AppendLine("using System.Reflection;")
65  .AppendLine("using Barotrauma;")
66  .AppendLine("using System.Runtime.CompilerServices;")
67  .AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]")
68 #if CLIENT
69  .AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
70 #elif SERVER
71  .AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
72 #endif
73  .ToString(),
74  ScriptParseOptions);
75 
76  private readonly string[] _publicizedAssembliesToLoad =
77  {
78  "BarotraumaCore.dll",
79 #if CLIENT
80  "Barotrauma.dll"
81 #elif SERVER
82  "DedicatedServer.dll"
83 #endif
84  };
85 
86 
87  private const string SCRIPT_FILE_REGEX = "*.cs";
88  private const string ASSEMBLY_FILE_REGEX = "*.dll";
89 
90  private readonly float _assemblyUnloadTimeoutSeconds = 6f;
91  private Guid _publicizedAssemblyLoader;
92  private readonly List<ContentPackage> _currentPackagesByLoadOrder = new();
93  private readonly Dictionary<ContentPackage, ImmutableList<ContentPackage>> _packagesDependencies = new();
94  private readonly Dictionary<ContentPackage, Guid> _loadedCompiledPackageAssemblies = new();
95  private readonly Dictionary<Guid, ContentPackage> _reverseLookupGuidList = new();
96  private readonly Dictionary<Guid, HashSet<IAssemblyPlugin>> _loadedPlugins = new ();
97  private readonly Dictionary<Guid, ImmutableHashSet<Type>> _pluginTypes = new(); // where Type : IAssemblyPlugin
98  private readonly Dictionary<ContentPackage, RunConfig> _packageRunConfigs = new();
99  private readonly Dictionary<Guid, ImmutableList<Type>> _luaRegisteredTypes = new();
100  private readonly AssemblyManager _assemblyManager;
101  private readonly LuaCsSetup _luaCsSetup;
102  private DateTime _assemblyUnloadStartTime;
103 
104 
105  #endregion
106 
107  #region PUBLIC_API
108 
109  #region LUA_EXTENSIONS
110 
117  public bool LuaTryRegisterPackageTypes(string name, bool caseSensitive = false)
118  {
119  if (!AssembliesLoaded)
120  return false;
121  var matchingPacks = _loadedCompiledPackageAssemblies
122  .Where(kvp => kvp.Key.Name.ToLowerInvariant().Contains(name.ToLowerInvariant()))
123  .Select(kvp => kvp.Value)
124  .ToImmutableList();
125  if (!matchingPacks.Any())
126  return false;
127  var types = matchingPacks
128  .Where(guid => !_luaRegisteredTypes.ContainsKey(guid))
129  .Select(guid => new KeyValuePair<Guid, ImmutableList<Type>>(
130  guid,
131  _assemblyManager.TryGetSubTypesFromACL(guid, out var types)
132  ? types.ToImmutableList()
133  : ImmutableList<Type>.Empty))
134  .ToImmutableList();
135  if (!types.Any())
136  return false;
137  foreach (var kvp in types)
138  {
139  _luaRegisteredTypes[kvp.Key] = kvp.Value;
140  foreach (Type type in kvp.Value)
141  {
142  MoonSharp.Interpreter.UserData.RegisterType(type);
143  }
144  }
145 
146  return true;
147  }
148 
149  #endregion
150 
154  public bool AssembliesLoaded { get; private set; }
155 
156 
160  public bool PluginsPreInit { get; private set; }
161 
165  public bool PluginsInitialized { get; private set; }
166 
170  public bool PluginsLoaded { get; private set; }
171 
172  public IEnumerable<ContentPackage> GetCurrentPackagesByLoadOrder() => _currentPackagesByLoadOrder;
173 
180  public bool TryGetPackageForPlugin<T>(out ContentPackage package) where T : IAssemblyPlugin
181  {
182  package = null;
183 
184  var t = typeof(T);
185  var guid = _pluginTypes
186  .Where(kvp => kvp.Value.Contains(t))
187  .Select(kvp => kvp.Key)
188  .FirstOrDefault(Guid.Empty);
189 
190  if (guid.Equals(Guid.Empty) || !_reverseLookupGuidList.ContainsKey(guid) || _reverseLookupGuidList[guid] is null)
191  return false;
192  package = _reverseLookupGuidList[guid];
193  return true;
194  }
195 
196 
203  public bool TryGetLoadedPluginsForPackage(ContentPackage package, out IEnumerable<IAssemblyPlugin> loadedPlugins)
204  {
205  loadedPlugins = null;
206  if (package is null || !_loadedCompiledPackageAssemblies.ContainsKey(package))
207  return false;
208  var guid = _loadedCompiledPackageAssemblies[package];
209  if (guid.Equals(Guid.Empty) || !_loadedPlugins.ContainsKey(guid))
210  return false;
211  loadedPlugins = _loadedPlugins[guid];
212  return true;
213  }
214 
218  public event Action OnDispose;
219 
220  [MethodImpl(MethodImplOptions.Synchronized)]
221  public void Dispose()
222  {
223  // send events for cleanup
224  try
225  {
226  OnDispose?.Invoke();
227  }
228  catch (Exception e)
229  {
230  ModUtils.Logging.PrintError($"Error while executing Dispose event: {e.Message}");
231  }
232 
233  // cleanup events
234  if (OnDispose is not null)
235  {
236  foreach (Delegate del in OnDispose.GetInvocationList())
237  {
238  OnDispose -= (del as System.Action);
239  }
240  }
241 
242  // cleanup plugins and assemblies
243  ReflectionUtils.ResetCache();
244  UnloadPlugins();
245  // try cleaning up the assemblies
246  _pluginTypes.Clear(); // remove assembly references
247  _loadedPlugins.Clear();
248  _publicizedAssemblyLoader = Guid.Empty;
249  _packagesDependencies.Clear();
250  _loadedCompiledPackageAssemblies.Clear();
251  _reverseLookupGuidList.Clear();
252  _packageRunConfigs.Clear();
253  _currentPackagesByLoadOrder.Clear();
254 
255  // lua cleanup
256  foreach (var kvp in _luaRegisteredTypes)
257  {
258  foreach (Type type in kvp.Value)
259  {
260  MoonSharp.Interpreter.UserData.UnregisterType(type);
261  }
262  }
263  _luaRegisteredTypes.Clear();
264 
265  _assemblyUnloadStartTime = DateTime.Now;
266  _publicizedAssemblyLoader = Guid.Empty;
267 
268  // we can't wait forever or app dies but we can try to be graceful
269  while (!_assemblyManager.TryBeginDispose())
270  {
271  Thread.Sleep(20); // give the assembly context unloader time to run (async)
272  if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
273  {
274  break;
275  }
276  }
277 
278  _assemblyUnloadStartTime = DateTime.Now;
279  Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies.
280  while (!_assemblyManager.FinalizeDispose())
281  {
282  Thread.Sleep(100); // give the garbage collector time to finalize the disposed assemblies.
283  if (_assemblyUnloadStartTime.AddSeconds(_assemblyUnloadTimeoutSeconds) > DateTime.Now)
284  {
285  break;
286  }
287  }
288 
289  _assemblyManager.OnAssemblyLoaded -= AssemblyManagerOnAssemblyLoaded;
290  _assemblyManager.OnAssemblyUnloading -= AssemblyManagerOnAssemblyUnloading;
291 
292  AssembliesLoaded = false;
293  GC.SuppressFinalize(this);
294  }
295 
300  public AssemblyLoadingSuccessState LoadAssemblyPackages()
301  {
302  if (AssembliesLoaded)
303  {
304  return AssemblyLoadingSuccessState.AlreadyLoaded;
305  }
306 
307  _assemblyManager.OnAssemblyLoaded += AssemblyManagerOnAssemblyLoaded;
308  _assemblyManager.OnAssemblyUnloading += AssemblyManagerOnAssemblyUnloading;
309 
310  // log error if some ACLs are still unloading (some assembly is still in use)
311  _assemblyManager.FinalizeDispose(); //Update lists
312  if (_assemblyManager.IsCurrentlyUnloading)
313  {
314  ModUtils.Logging.PrintMessage($"The below ACLs are still unloading:");
315  foreach (var wkref in _assemblyManager.StillUnloadingACLs)
316  {
317  if (wkref.TryGetTarget(out var tgt))
318  {
319  ModUtils.Logging.PrintMessage($"ACL Name: {tgt.FriendlyName}");
320  foreach (Assembly assembly in tgt.Assemblies)
321  {
322  ModUtils.Logging.PrintMessage($"-- Assembly: {assembly.GetName()}");
323  }
324  }
325  }
326  }
327 
328  ImmutableList<Assembly> publicizedAssemblies = ImmutableList<Assembly>.Empty;
329  List<string> publicizedAssembliesLocList = new();
330 
331  foreach (string dllName in _publicizedAssembliesToLoad)
332  {
333  GetFiles(publicizedAssembliesLocList, dllName);
334  }
335 
336  void GetFiles(List<string> list, string searchQuery)
337  {
338  bool workshopFirst = _luaCsSetup.Config.PreferToUseWorkshopLuaSetup || LuaCsSetup.IsRunningInsideWorkshop;
339 
340  var publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized");
341 
342  // if using workshop lua setup is checked, try to use the publicized assemblies in the content package there instead.
343  if (workshopFirst)
344  {
345  var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId);
346  if (pck is not null)
347  {
348  publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized");
349  }
350  }
351 
352  try
353  {
354  list.AddRange(Directory.GetFiles(publicizedDir, searchQuery));
355  }
356  // no directory found, use the other one
357  catch (DirectoryNotFoundException)
358  {
359  if (workshopFirst)
360  {
361  ModUtils.Logging.PrintError($"Unable to find <LuaCsPackage>/Binary/Publicized/ . Using Game folder instead.");
362  publicizedDir = Path.Combine(Environment.CurrentDirectory, "Publicized");
363  }
364  else
365  {
366  ModUtils.Logging.PrintError($"Unable to find <GameFolder>/Publicized/ . Using LuaCsPackage folder instead.");
367  var pck = LuaCsSetup.GetPackage(LuaCsSetup.LuaForBarotraumaId);
368  if (pck is not null)
369  {
370  publicizedDir = Path.Combine(pck.Dir, "Binary", "Publicized");
371  }
372  }
373 
374  // search for assemblies
375  list.AddRange(Directory.GetFiles(publicizedDir, searchQuery));
376  }
377  }
378 
379  // try load them into an acl
380  var loadState = _assemblyManager.LoadAssembliesFromLocations(publicizedAssembliesLocList, "luacs_publicized_assemblies", ref _publicizedAssemblyLoader);
381 
382  // loaded
383  if (loadState is AssemblyLoadingSuccessState.Success)
384  {
385  if (_assemblyManager.TryGetACL(_publicizedAssemblyLoader, out var acl))
386  {
387  publicizedAssemblies = acl.Acl.Assemblies.ToImmutableList();
388  _assemblyManager.SetACLToTemplateMode(_publicizedAssemblyLoader);
389  }
390  }
391 
392 
393  // get packages
394  IEnumerable<ContentPackage> packages = BuildPackagesList();
395 
396  // check and load config
397  _packageRunConfigs.AddRange(packages
398  .Select(p => new KeyValuePair<ContentPackage, RunConfig>(p, GetRunConfigForPackage(p)))
399  .ToDictionary(p => p.Key, p=> p.Value));
400 
401  // filter not to be loaded
402  var cpToRunA = _packageRunConfigs
403  .Where(kvp => ShouldRunPackage(kvp.Key, kvp.Value))
404  .Select(kvp => kvp.Key)
405  .ToHashSet();
406 
407  //-- filter and remove duplicate mods, prioritize /LocalMods/
408  HashSet<string> cpNames = new();
409  HashSet<string> duplicateNames = new();
410 
411  // search
412  foreach (ContentPackage package in cpToRunA)
413  {
414  if (cpNames.Contains(package.Name))
415  {
416  if (!duplicateNames.Contains(package.Name))
417  {
418  duplicateNames.Add(package.Name);
419  }
420  }
421  else
422  {
423  cpNames.Add(package.Name);
424  }
425  }
426 
427  // remove
428  foreach (string name in duplicateNames)
429  {
430  var duplCpList = cpToRunA
431  .Where(p => p.Name.Equals(name))
432  .ToHashSet();
433 
434  if (duplCpList.Count < 2) // one or less found
435  continue;
436 
437  ContentPackage toKeep = null;
438  foreach (ContentPackage package in duplCpList)
439  {
440  if (package.Dir.Contains("LocalMods"))
441  {
442  toKeep = package;
443  break;
444  }
445  }
446 
447  toKeep ??= duplCpList.First();
448 
449  duplCpList.Remove(toKeep); // remove all but this one
450  cpToRunA.RemoveWhere(p => duplCpList.Contains(p));
451  }
452 
453  var cpToRun = cpToRunA.ToImmutableList();
454 
455  // build dependencies map
456  bool reliableMap = TryBuildDependenciesMap(cpToRun, out var packDeps);
457  if (!reliableMap)
458  {
459  ModUtils.Logging.PrintMessage($"{nameof(CsPackageManager)}: Unable to create reliable dependencies map.");
460  }
461 
462  _packagesDependencies.AddRange(packDeps.ToDictionary(
463  kvp => kvp.Key,
464  kvp => kvp.Value.ToImmutableList())
465  );
466 
467  List<ContentPackage> packagesToLoadInOrder = new();
468 
469  // build load order
470  if (reliableMap && OrderAndFilterPackagesByDependencies(
471  _packagesDependencies,
472  out var readyToLoad,
473  out var cannotLoadPackages))
474  {
475  packagesToLoadInOrder.AddRange(readyToLoad);
476  if (cannotLoadPackages is not null)
477  {
478  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the following mods due to dependency errors:");
479  foreach (var pair in cannotLoadPackages)
480  {
481  ModUtils.Logging.PrintError($"Package: {pair.Key.Name} | Reason: {pair.Value}");
482  }
483  }
484  }
485  else
486  {
487  // use unsorted list on failure and send error message.
488  packagesToLoadInOrder.AddRange(_packagesDependencies.Select( p=> p.Key));
489  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to create a reliable load order. Defaulting to unordered loading!");
490  }
491 
492  // get assemblies and scripts' filepaths from packages
493  var toLoad = packagesToLoadInOrder
494  .Select(cp => new KeyValuePair<ContentPackage, LoadableData>(
495  cp,
496  new LoadableData(
497  TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null,
498  TryScanPackageForScripts(cp, out var list2) ? list2 : null,
499  GetRunConfigForPackage(cp))))
500  .ToImmutableDictionary();
501 
502  HashSet<ContentPackage> badPackages = new();
503  foreach (var pair in toLoad)
504  {
505  // check if unloadable
506  if (badPackages.Contains(pair.Key))
507  continue;
508 
509  // try load binary assemblies
510  var id = Guid.Empty; // id for the ACL for this package defined by AssemblyManager.
511  AssemblyLoadingSuccessState successState;
512  if (pair.Value.AssembliesFilePaths is not null && pair.Value.AssembliesFilePaths.Any())
513  {
514  ModUtils.Logging.PrintMessage($"Loading assemblies for CPackage {pair.Key.Name}");
515 #if DEBUG
516  foreach (string assembliesFilePath in pair.Value.AssembliesFilePaths)
517  {
518  ModUtils.Logging.PrintMessage($"Found assemblies located at {Path.GetFullPath(ModUtils.IO.SanitizePath(assembliesFilePath))}");
519  }
520 #endif
521 
522  successState = _assemblyManager.LoadAssembliesFromLocations(pair.Value.AssembliesFilePaths, pair.Key.Name, ref id);
523 
524  // error handling
525  if (successState is not AssemblyLoadingSuccessState.Success)
526  {
527  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the binary assemblies for package {pair.Key.Name}. Error: {successState.ToString()}");
528  UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
529  continue;
530  }
531  }
532 
533  // try compile scripts to assemblies
534  if (pair.Value.ScriptsFilePaths is not null && pair.Value.ScriptsFilePaths.Any())
535  {
536  ModUtils.Logging.PrintMessage($"Loading scripts for CPackage {pair.Key.Name}");
537  List<SyntaxTree> syntaxTrees = new();
538 
539  syntaxTrees.Add(GetPackageScriptImports());
540  bool abortPackage = false;
541  // load scripts data from files
542  foreach (string scriptPath in pair.Value.ScriptsFilePaths)
543  {
544  var state = ModUtils.IO.GetOrCreateFileText(scriptPath, out string fileText, null, false);
545  // could not load file data
546  if (state is not ModUtils.IO.IOActionResultState.Success)
547  {
548  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {state.ToString()}");
549  UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
550  abortPackage = true;
551  break;
552  }
553 
554  try
555  {
556  CancellationToken token = new();
557  syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(fileText, ScriptParseOptions, scriptPath, Encoding.Default, token));
558  // cancel if parsing failed
559  if (token.IsCancellationRequested)
560  {
561  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: Syntax Parse Error.");
562  UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
563  abortPackage = true;
564  break;
565  }
566  }
567  catch (Exception e)
568  {
569  // unknown error
570  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to load the script files for package {pair.Key.Name}. Error: {e.Message}");
571  UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
572  abortPackage = true;
573  break;
574  }
575 
576  }
577 
578  if (abortPackage)
579  continue;
580 
581  // try compile
582  successState = _assemblyManager.LoadAssemblyFromMemory(
583  pair.Value.config.UseInternalAssemblyName ? "CompiledAssembly" : pair.Key.Name.Replace(" ",""),
584  syntaxTrees,
585  null,
586  CompilationOptions,
587  pair.Key.Name,
588  ref id,
589  pair.Value.config.UseNonPublicizedAssemblies ? null : publicizedAssemblies);
590 
591  if (successState is not AssemblyLoadingSuccessState.Success)
592  {
593  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Unable to compile script assembly for package {pair.Key.Name}. Error: {successState.ToString()}");
594  UpdatePackagesToDisable(ref badPackages, pair.Key, _packagesDependencies);
595  continue;
596  }
597  }
598 
599  // something was loaded, add to index
600  if (id != Guid.Empty)
601  {
602  ModUtils.Logging.PrintMessage($"Assemblies from CPackage {pair.Key.Name} loaded with Guid {id}.");
603  _loadedCompiledPackageAssemblies.Add(pair.Key, id);
604  _reverseLookupGuidList.Add(id, pair.Key);
605  }
606  }
607 
608  // update loaded packages to exclude bad packages
609  _currentPackagesByLoadOrder.AddRange(toLoad
610  .Where(p => !badPackages.Contains(p.Key))
611  .Select(p => p.Key));
612 
613  // build list of plugins
614  foreach (var pair in _loadedCompiledPackageAssemblies)
615  {
616  if (_assemblyManager.TryGetSubTypesFromACL<IAssemblyPlugin>(pair.Value, out var types))
617  {
618  _pluginTypes[pair.Value] = types.ToImmutableHashSet();
619  foreach (var type in _pluginTypes[pair.Value])
620  {
621  ModUtils.Logging.PrintMessage($"Loading type: {type.Name}");
622  }
623  }
624  }
625 
626  this.AssembliesLoaded = true;
627  return AssemblyLoadingSuccessState.Success;
628 
629 
630  bool ShouldRunPackage(ContentPackage package, RunConfig config)
631  {
632  return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced())
633  || (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard());
634  }
635 
636  void UpdatePackagesToDisable(ref HashSet<ContentPackage> set,
637  ContentPackage newDisabledPackage,
638  IEnumerable<KeyValuePair<ContentPackage, ImmutableList<ContentPackage>>> dependenciesMap)
639  {
640  set.Add(newDisabledPackage);
641  foreach (var package in dependenciesMap)
642  {
643  if (package.Value.Contains(newDisabledPackage))
644  set.Add(newDisabledPackage);
645  }
646  }
647  }
648 
652  public void RunPluginsInit()
653  {
654  if (!AssembliesLoaded)
655  {
656  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without any loaded assemblies!");
657  return;
658  }
659 
660  if (!PluginsInitialized)
661  {
662  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' Initialize() without type instantiation!");
663  return;
664  }
665 
666  if (PluginsLoaded)
667  return;
668 
669  foreach (var contentPlugins in _loadedPlugins)
670  {
671  // init
672  foreach (var plugin in contentPlugins.Value)
673  {
674  TryRun(() => plugin.Initialize(), $"{nameof(IAssemblyPlugin.Initialize)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
675  }
676  }
677 
678  foreach (var contentPlugins in _loadedPlugins)
679  {
680  // load complete
681  foreach (var plugin in contentPlugins.Value)
682  {
683  TryRun(() => plugin.OnLoadCompleted(), $"{nameof(IAssemblyPlugin.OnLoadCompleted)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
684  }
685  }
686 
687  PluginsLoaded = true;
688  }
689 
693  public void RunPluginsPreInit()
694  {
695  if (!AssembliesLoaded)
696  {
697  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without any loaded assemblies!");
698  return;
699  }
700 
701  if (!PluginsInitialized)
702  {
703  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to call plugins' PreInitPatching() without type initialization!");
704  return;
705  }
706 
707  if (PluginsPreInit)
708  {
709  return;
710  }
711 
712  foreach (var contentPlugins in _loadedPlugins)
713  {
714  // init
715  foreach (var plugin in contentPlugins.Value)
716  {
717  TryRun(() => plugin.PreInitPatching(), $"{nameof(IAssemblyPlugin.PreInitPatching)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
718  }
719  }
720 
721  PluginsPreInit = true;
722  }
723 
728  public void InstantiatePlugins(bool force = false)
729  {
730  if (!AssembliesLoaded)
731  {
732  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to instantiate plugins without any loaded assemblies!");
733  return;
734  }
735 
736  if (PluginsInitialized)
737  {
738  if (force)
739  UnloadPlugins();
740  else
741  {
742  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Attempted to load plugins when they were already loaded!");
743  return;
744  }
745  }
746 
747  foreach (var pair in _pluginTypes)
748  {
749  // instantiate
750  foreach (Type type in pair.Value)
751  {
752  if (!_loadedPlugins.ContainsKey(pair.Key))
753  _loadedPlugins.Add(pair.Key, new());
754  else if (_loadedPlugins[pair.Key] is null)
755  _loadedPlugins[pair.Key] = new();
756  IAssemblyPlugin plugin = null;
757  try
758  {
759  plugin = (IAssemblyPlugin)Activator.CreateInstance(type);
760  _loadedPlugins[pair.Key].Add(plugin);
761  }
762  catch (Exception e)
763  {
764  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while instantiating plugin of type {type}. Now disposing...");
765  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
766 
767  if (plugin is not null)
768  {
769  // ReSharper disable once AccessToModifiedClosure
770  TryRun(() => plugin?.Dispose(), nameof(IAssemblyPlugin.Dispose), type.FullName ?? type.Name);
771  plugin = null;
772  }
773  }
774  }
775  }
776 
777  PluginsInitialized = true;
778  }
779 
784  public void UnloadPlugins()
785  {
786  foreach (var contentPlugins in _loadedPlugins)
787  {
788  foreach (var plugin in contentPlugins.Value)
789  {
790  TryRun(() => plugin.Dispose(), $"{nameof(IAssemblyPlugin.Dispose)}", $"CP: {_reverseLookupGuidList[contentPlugins.Key].Name} Plugin: {plugin.GetType().Name}");
791  }
792  contentPlugins.Value.Clear();
793  }
794 
795  _loadedPlugins.Clear();
796 
797  PluginsInitialized = false;
798  PluginsPreInit = false;
799  PluginsLoaded = false;
800  }
801 
802 
810  public static bool GetOrCreateRunConfig(ContentPackage package, out RunConfig config)
811  {
812  var path = System.IO.Path.Combine(Path.GetFullPath(package.Dir), "CSharp", "RunConfig.xml");
813  if (!File.Exists(path))
814  {
815  config = new RunConfig(true).Sanitize();
816  return false;
817  }
818  return ModUtils.IO.LoadOrCreateTypeXml(out config, path, () => new RunConfig(true).Sanitize(), false);
819  }
820 
821  #endregion
822 
823  #region INTERNALS
824 
825  private void TryRun(Action action, string messageMethodName, string messageTypeName)
826  {
827  try
828  {
829  action?.Invoke();
830  }
831  catch (Exception e)
832  {
833  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Error while running {messageMethodName}() on plugin of type {messageTypeName}");
834  ModUtils.Logging.PrintError($"{nameof(CsPackageManager)}: Details: {e.Message} | {e.InnerException}");
835  }
836  }
837 
838  private void AssemblyManagerOnAssemblyUnloading(Assembly assembly)
839  {
840  ReflectionUtils.RemoveAssemblyFromCache(assembly);
841  }
842 
843  private void AssemblyManagerOnAssemblyLoaded(Assembly assembly)
844  {
845  //ReflectionUtils.AddNonAbstractAssemblyTypes(assembly);
846  // As ReflectionUtils.GetDerivedNonAbstract is only used for Prefabs & Barotrauma-specific implementing types,
847  // we can safely not register System/Core assemblies.
848  if (assembly.FullName is not null && assembly.FullName.StartsWith("System."))
849  return;
850  ReflectionUtils.AddNonAbstractAssemblyTypes(assembly, true);
851  }
852 
853  internal CsPackageManager([NotNull] AssemblyManager assemblyManager, [NotNull] LuaCsSetup luaCsSetup)
854  {
855  this._assemblyManager = assemblyManager;
856  this._luaCsSetup = luaCsSetup;
857  }
858 
860  {
861  this.Dispose();
862  }
863 
864  private static bool TryScanPackageForScripts(ContentPackage package, out ImmutableList<string> scriptFilePaths)
865  {
866  string pathShared = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", "Shared");
867  string pathArch = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "CSharp", ARCHITECTURE_TARGET);
868 
869  List<string> files = new();
870 
871  if (Directory.Exists(pathShared))
872  files.AddRange(Directory.GetFiles(pathShared, SCRIPT_FILE_REGEX, SearchOption.AllDirectories));
873  if (Directory.Exists(pathArch))
874  files.AddRange(Directory.GetFiles(pathArch, SCRIPT_FILE_REGEX, SearchOption.AllDirectories));
875 
876  if (files.Count > 0)
877  {
878  scriptFilePaths = files.ToImmutableList();
879  return true;
880  }
881  scriptFilePaths = ImmutableList<string>.Empty;
882  return false;
883  }
884 
885  private static bool TryScanPackagesForAssemblies(ContentPackage package, out ImmutableList<string> assemblyFilePaths)
886  {
887  string path = Path.Combine(ModUtils.IO.GetContentPackageDir(package), "bin", ARCHITECTURE_TARGET, PLATFORM_TARGET);
888 
889  if (!Directory.Exists(path))
890  {
891  assemblyFilePaths = ImmutableList<string>.Empty;
892  return false;
893  }
894 
895  assemblyFilePaths = System.IO.Directory.GetFiles(path, ASSEMBLY_FILE_REGEX, SearchOption.AllDirectories)
896  .ToImmutableList();
897  return assemblyFilePaths.Count > 0;
898  }
899 
900  private static RunConfig GetRunConfigForPackage(ContentPackage package)
901  {
902  if (!GetOrCreateRunConfig(package, out var config))
903  config.AutoGenerated = true;
904  return config;
905  }
906 
907  private IEnumerable<ContentPackage> BuildPackagesList()
908  {
909  // get unique list of content packages.
910  // Note: there is an old issue where the AllPackages group
911  // would sometimes not contain packages downloaded from the host, so we union enabled.
912  return ContentPackageManager.AllPackages.Union(ContentPackageManager.EnabledPackages.All).Where(pack => !pack.Name.ToLowerInvariant().Equals("vanilla"));
913  }
914 
915 
916  private static SyntaxTree GetPackageScriptImports() => BaseAssemblyImports;
917 
918 
925  private static bool TryBuildDependenciesMap(ImmutableList<ContentPackage> packages, out Dictionary<ContentPackage, List<ContentPackage>> dependenciesMap)
926  {
927  bool reliableMap = true; // remains true if all deps were found.
928  dependenciesMap = new();
929  foreach (var package in packages)
930  {
931  dependenciesMap.Add(package, new());
932  if (GetOrCreateRunConfig(package, out var config))
933  {
934  if (config.Dependencies is null || !config.Dependencies.Any())
935  continue;
936 
937  foreach (RunConfig.Dependency dependency in config.Dependencies)
938  {
939  ContentPackage dep = packages.FirstOrDefault(p =>
940  (dependency.SteamWorkshopId != 0 && p.TryExtractSteamWorkshopId(out var steamWorkshopId)
941  && steamWorkshopId.Value == dependency.SteamWorkshopId)
942  || (!dependency.PackageName.IsNullOrWhiteSpace() && p.Name.ToLowerInvariant().Contains(dependency.PackageName.ToLowerInvariant())), null);
943 
944  if (dep is not null)
945  {
946  dependenciesMap[package].Add(dep);
947  }
948  else
949  {
950  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!");
951  reliableMap = false;
952  }
953  }
954  }
955  }
956 
957  return reliableMap;
958  }
959 
970  private static bool OrderAndFilterPackagesByDependencies(
971  Dictionary<ContentPackage, ImmutableList<ContentPackage>> packages,
972  out IEnumerable<ContentPackage> readyToLoad,
973  out IEnumerable<KeyValuePair<ContentPackage, string>> cannotLoadPackages,
974  Func<ContentPackage, bool> packageChecksPredicate = null)
975  {
976  HashSet<ContentPackage> completedPackages = new();
977  List<ContentPackage> readyPackages = new();
978  Dictionary<ContentPackage, string> unableToLoad = new();
979  HashSet<ContentPackage> currentNodeChain = new();
980 
981  readyToLoad = readyPackages;
982 
983  try
984  {
985  foreach (var toProcessPack in packages)
986  {
987  ProcessPackage(toProcessPack.Key, toProcessPack.Value);
988  }
989 
990  PackageProcRet ProcessPackage(ContentPackage packageToProcess, IEnumerable<ContentPackage> dependencies)
991  {
992  //cyclic handling
993  if (unableToLoad.ContainsKey(packageToProcess))
994  {
995  return PackageProcRet.BadPackage;
996  }
997 
998  // already processed
999  if (completedPackages.Contains(packageToProcess))
1000  {
1001  return PackageProcRet.AlreadyCompleted;
1002  }
1003 
1004  // cyclic check
1005  if (currentNodeChain.Contains(packageToProcess))
1006  {
1007  StringBuilder sb = new();
1008  sb.AppendLine("Error: Cyclic Dependency. ")
1009  .Append(
1010  "The following ContentPackages rely on eachother in a way that makes it impossible to know which to load first! ")
1011  .Append(
1012  "Note: the package listed twice shows where the cycle starts/ends and is not necessarily the problematic package.");
1013  int i = 0;
1014  foreach (var package in currentNodeChain)
1015  {
1016  i++;
1017  sb.AppendLine($"{i}. {package.Name}");
1018  }
1019 
1020  sb.AppendLine($"{i}. {packageToProcess.Name}");
1021  unableToLoad.Add(packageToProcess, sb.ToString());
1022  completedPackages.Add(packageToProcess);
1023  return PackageProcRet.BadPackage;
1024  }
1025 
1026  if (packageChecksPredicate is not null && !packageChecksPredicate.Invoke(packageToProcess))
1027  {
1028  unableToLoad.Add(packageToProcess, $"Unable to load package {packageToProcess.Name} due to failing checks.");
1029  completedPackages.Add(packageToProcess);
1030  return PackageProcRet.BadPackage;
1031  }
1032 
1033  currentNodeChain.Add(packageToProcess);
1034 
1035  foreach (ContentPackage dependency in dependencies)
1036  {
1037  // The mod lists a dependent that was not found during the discovery phase.
1038  if (!packages.ContainsKey(dependency))
1039  {
1040  // search to see if it's enabled
1041  if (!ContentPackageManager.EnabledPackages.All.Contains(dependency))
1042  {
1043  // present warning but allow loading anyways, better to let the user just disable the package if it's really an issue.
1044  ModUtils.Logging.PrintWarning(
1045  $"Warning: the ContentPackage of {packageToProcess.Name} requires the Dependency {dependency.Name} but this package wasn't found in the enabled mods list!");
1046  }
1047 
1048  continue;
1049  }
1050 
1051  var ret = ProcessPackage(dependency, packages[dependency]);
1052 
1053  if (ret is PackageProcRet.BadPackage)
1054  {
1055  if (!unableToLoad.ContainsKey(packageToProcess))
1056  {
1057  unableToLoad.Add(packageToProcess, $"Error: Dependency failure. Failed to load {dependency.Name}");
1058  }
1059  currentNodeChain.Remove(packageToProcess);
1060  if (!completedPackages.Contains(packageToProcess))
1061  {
1062  completedPackages.Add(packageToProcess);
1063  }
1064  return PackageProcRet.BadPackage;
1065  }
1066  }
1067 
1068  currentNodeChain.Remove(packageToProcess);
1069  completedPackages.Add(packageToProcess);
1070  readyPackages.Add(packageToProcess);
1071  return PackageProcRet.Completed;
1072  }
1073  }
1074  catch (Exception e)
1075  {
1076  ModUtils.Logging.PrintError($"Error while generating dependency loading order! Exception: {e.Message}");
1077 #if DEBUG
1078  ModUtils.Logging.PrintError($"Stack Trace: {e.StackTrace}");
1079 #endif
1080  cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null;
1081  return false;
1082  }
1083  cannotLoadPackages = unableToLoad.Any() ? unableToLoad : null;
1084  return true;
1085  }
1086 
1087  private enum PackageProcRet : byte
1088  {
1089  AlreadyCompleted,
1090  Completed,
1091  BadPackage
1092  }
1093 
1094  private record LoadableData(ImmutableList<string> AssembliesFilePaths, ImmutableList<string> ScriptsFilePaths, RunConfig config);
1095 
1096  #endregion
1097 }
Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlu...
void RunPluginsInit()
Executes instantiated plugins' Initialize() and OnLoadCompleted() methods.
void RunPluginsPreInit()
Executes instantiated plugins' PreInitPatching() method.
bool TryGetLoadedPluginsForPackage(ContentPackage package, out IEnumerable< IAssemblyPlugin > loadedPlugins)
Tries to get the loaded plugins for a given package.
Action OnDispose
Called when clean up is being performed. Use when relying on or making use of references from this ma...
bool AssembliesLoaded
Whether or not assemblies have been loaded.
void InstantiatePlugins(bool force=false)
Initializes plugin types that are registered.
AssemblyLoadingSuccessState LoadAssemblyPackages()
Begins the loading process of scanning packages for scripts and binary assemblies,...
bool TryGetPackageForPlugin< T >(out ContentPackage package)
Tries to find the content package that a given plugin belongs to.
static bool GetOrCreateRunConfig(ContentPackage package, out RunConfig config)
Gets the RunConfig.xml for the given package located at [cp_root]/CSharp/RunConfig....
bool PluginsPreInit
Whether or not loaded plugins had their preloader run.
bool LuaTryRegisterPackageTypes(string name, bool caseSensitive=false)
Searches for all types in all loaded assemblies from content packages who's names contain the name st...
bool PluginsInitialized
Whether or not plugins' types have been instantiated.
bool PluginsLoaded
Whether or not plugins are fully loaded.
IEnumerable< ContentPackage > GetCurrentPackagesByLoadOrder()
bool AutoGenerated
Definition: RunConfig.cs:46
RunConfig Sanitize()
Definition: RunConfig.cs:76
bool IsForced()
Definition: RunConfig.cs:110
bool IsForcedOrStandard()