diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 10988b54d..125cb9995 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -281,8 +281,6 @@ namespace Barotrauma { UserData.UnregisterType(type, true); } - - PluginPackageManager.UnloadPlugins(); // stop plugin code execution if (Lua?.Globals is not null) { @@ -309,6 +307,9 @@ namespace Barotrauma // we can only unload assemblies after clearing ModStore/references. PluginPackageManager.Dispose(); +#pragma warning disable CS0618 + ACsMod.LoadedMods.Clear(); +#pragma warning restore CS0618 Game = new LuaGame(); Networking = new LuaCsNetworking(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs index 962f93371..089cea715 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs @@ -9,6 +9,7 @@ using Barotrauma; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.CodeAnalysis; +using Microsoft.Xna.Framework; namespace Barotrauma; @@ -26,6 +27,15 @@ public static class ModUtils LuaCsLogger.LogMessage($"[Client] {s}"); #endif } + + public static void PrintWarning(string s) + { +#if SERVER + LuaCsLogger.Log($"[Server] {s}", Color.Yellow); +#else + LuaCsLogger.Log($"[Client] {s}", Color.Yellow); +#endif + } public static void PrintError(string s) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs index 09133aad6..47be0036c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyManager.cs @@ -481,6 +481,7 @@ public class AssemblyManager try { _subTypesLookupCache.Clear(); + _defaultContextTypes = _defaultContextTypes.Clear(); foreach (KeyValuePair loadedAcl in LoadedACLs) { @@ -506,6 +507,7 @@ public class AssemblyManager UnloadingACLs.Add(new WeakReference(loadedAcl.Value.Acl, true)); loadedAcl.Value.ClearTypesList(); loadedAcl.Value.Acl.Unload(); + loadedAcl.Value.ClearACLRef(); OnACLUnload?.Invoke(loadedAcl.Value.Id); } } @@ -534,6 +536,8 @@ public class AssemblyManager OpsLockUnloaded.EnterUpgradeableReadLock(); try { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls. + GC.WaitForPendingFinalizers(); List> toRemove = new(); foreach (WeakReference weakReference in UnloadingACLs) { @@ -666,6 +670,7 @@ public class AssemblyManager _subTypesLookupCache.Clear(); UnloadingACLs.Add(new WeakReference(acl.Acl, true)); acl.Acl.Unload(); + acl.ClearACLRef(); OnACLUnload?.Invoke(acl.Id); return true; @@ -740,8 +745,8 @@ public class AssemblyManager private ImmutableDictionary _defaultContextTypes; private readonly ConcurrentDictionary LoadedACLs = new(); private readonly List> UnloadingACLs= new(); - private readonly ReaderWriterLockSlim OpsLockLoaded = new ReaderWriterLockSlim(); - private readonly ReaderWriterLockSlim OpsLockUnloaded = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim OpsLockLoaded = new (); + private readonly ReaderWriterLockSlim OpsLockUnloaded = new (); #endregion @@ -752,7 +757,7 @@ public class AssemblyManager { public readonly Guid Id; private ImmutableDictionary _assembliesTypes = ImmutableDictionary.Empty; - public readonly MemoryFileAssemblyContextLoader Acl; + public MemoryFileAssemblyContextLoader Acl { get; private set; } internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName) { @@ -763,6 +768,14 @@ public class AssemblyManager }; } public ImmutableDictionary AssembliesTypes => _assembliesTypes; + + /// + /// Warning: For use by the Assembly Manager only! Do not call this method otherwise. + /// + internal void ClearACLRef() + { + Acl = null; + } /// /// Rebuild the list of types from assemblies loaded in the AsmCtxLoader. diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs index f041279e4..6b9397e3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs @@ -5,6 +5,7 @@ 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.Steam; @@ -73,7 +74,7 @@ public sealed class CsPackageManager : IDisposable private const string SCRIPT_FILE_REGEX = "*.cs"; private const string ASSEMBLY_FILE_REGEX = "*.dll"; - private readonly float _assemblyUnloadTimeoutSeconds = 4f; + private readonly float _assemblyUnloadTimeoutSeconds = 6f; private Guid _publicizedAssemblyLoader; private readonly List _currentPackagesByLoadOrder = new(); private readonly Dictionary> _packagesDependencies = new(); @@ -203,10 +204,19 @@ public sealed class CsPackageManager : IDisposable /// public event Action OnDispose; + [MethodImpl(MethodImplOptions.Synchronized)] public void Dispose() { // send events for cleanup - OnDispose?.Invoke(); + try + { + OnDispose?.Invoke(); + } + catch (Exception e) + { + ModUtils.Logging.PrintError($"Error while executing Dispose event: {e.Message}"); + } + // cleanup events if (OnDispose is not null) { @@ -219,10 +229,15 @@ public sealed class CsPackageManager : IDisposable // 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) @@ -240,6 +255,7 @@ public sealed class CsPackageManager : IDisposable // 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; @@ -247,8 +263,10 @@ public sealed class CsPackageManager : IDisposable } _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; @@ -258,17 +276,8 @@ public sealed class CsPackageManager : IDisposable _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); + GC.SuppressFinalize(this); } /// @@ -288,16 +297,16 @@ public sealed class CsPackageManager : IDisposable // 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."); + ModUtils.Logging.PrintWarning($"WARNING: Some mods from a previous session (lobby) are still loaded! This may result in undefined behaviour!\nIf you notice any odd behaviour that only occurs after multiple lobbies, please restart your game."); foreach (var wkref in _assemblyManager.StillUnloadingACLs) { - ModUtils.Logging.PrintError($"The below ACL is still unloading:"); + ModUtils.Logging.PrintWarning($"The below ACL is still unloading:"); if (wkref.TryGetTarget(out var tgt)) { - ModUtils.Logging.PrintError($"ACL Name: {tgt.Name}"); + ModUtils.Logging.PrintWarning($"ACL Name: {tgt.Name}"); foreach (Assembly assembly in tgt.Assemblies) { - ModUtils.Logging.PrintError($"-- Assembly: {assembly.GetName()}"); + ModUtils.Logging.PrintWarning($"-- Assembly: {assembly.GetName()}"); } } } @@ -480,7 +489,8 @@ public sealed class CsPackageManager : IDisposable cp, new LoadableData( TryScanPackagesForAssemblies(cp, out var list1) ? list1 : null, - TryScanPackageForScripts(cp, out var list2) ? list2 : null))) + TryScanPackageForScripts(cp, out var list2) ? list2 : null, + GetRunConfigForPackage(cp)))) .ToImmutableDictionary(); HashSet badPackages = new(); @@ -568,7 +578,7 @@ public sealed class CsPackageManager : IDisposable syntaxTrees, null, CompilationOptions, - pair.Key.Name, ref id, publicizedAssemblies); + pair.Key.Name, ref id, pair.Value.config.UseNonPublicizedAssemblies ? null : publicizedAssemblies); if (successState is not AssemblyLoadingSuccessState.Success) { @@ -1077,7 +1087,7 @@ public sealed class CsPackageManager : IDisposable BadPackage } - private record LoadableData(ImmutableList AssembliesFilePaths, ImmutableList ScriptsFilePaths); + private record LoadableData(ImmutableList AssembliesFilePaths, ImmutableList ScriptsFilePaths, RunConfig config); #endregion } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs index 7dde35577..a6222bcb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs @@ -34,6 +34,7 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true) { this._assemblyManager = assemblyManager; + base.Unloading += OnUnload; } @@ -278,10 +279,11 @@ public class MemoryFileAssemblyContextLoader : AssemblyLoadContext } - public new void Unload() + private void OnUnload(AssemblyLoadContext alc) { CompiledAssembly = null; CompiledAssemblyImage = null; - base.Unload(); + _dependencyResolvers.Clear(); + base.Unloading -= OnUnload; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs index 7ca448edd..4d8505d25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Xml.Serialization; namespace Barotrauma; @@ -9,12 +10,16 @@ public sealed class RunConfig /// /// How should scripts be run on the server. /// - [XmlElement(ElementName = "Server")] public string Server; + [XmlElement(ElementName = "Server")] + [DefaultValue("Standard")] + public string Server; /// /// How should scripts be run on the client. /// - [XmlElement(ElementName = "Client")] public string Client; + [XmlElement(ElementName = "Client")] + [DefaultValue("Standard")] + public string Client; /// /// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency"). @@ -23,7 +28,14 @@ public sealed class RunConfig [XmlArrayItem(ElementName = "Dependency", IsNullable = true, Type = typeof(Dependency))] [XmlArray] public Dependency[] Dependencies { get; set; } - + + /// + /// Compiles the mod using non-publicized assemblies. + /// + [XmlElement(ElementName = "UseNonPublicizedAssemblies")] + [DefaultValue(false)] + public bool UseNonPublicizedAssemblies { get; set; } + [XmlElement(ElementName = "AutoGenerated")] public bool AutoGenerated { get; set; } @@ -33,6 +45,7 @@ public sealed class RunConfig if (autoGenerated) { (Client, Server) = ("Standard", "Standard"); + UseNonPublicizedAssemblies = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs index ac8c601ac..24d015df4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ReflectionUtils.cs @@ -10,8 +10,7 @@ namespace Barotrauma { public static class ReflectionUtils { - private static readonly ConcurrentDictionary> CachedNonAbstractTypes - = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> CachedNonAbstractTypes = new(); private static readonly ConcurrentDictionary> TypeSearchCache = new(); public static IEnumerable GetDerivedNonAbstract()