Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MemoryJanitor v1.3.0
plugins/MemoryJanitor.dll
Decompiled 5 days agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Profiling; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MemoryJanitor")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.3.0.0")] [assembly: AssemblyInformationalVersion("1.3.0")] [assembly: AssemblyProduct("MemoryJanitor")] [assembly: AssemblyTitle("MemoryJanitor")] [assembly: AssemblyVersion("1.3.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MemoryJanitor { [BepInPlugin("lcmemfix.MemoryJanitor", "MemoryJanitor", "1.3.0")] public class Plugin : BaseUnityPlugin { public const string Guid = "lcmemfix.MemoryJanitor"; internal static ManualLogSource Log; internal static Runner Host; internal static ConfigEntry<bool> UnloadAudio; internal static ConfigEntry<float> MinClipSeconds; internal static ConfigEntry<bool> IncludePreloaded; internal static ConfigEntry<float> SweepSeconds; internal static ConfigEntry<float> IdleMinutes; internal static ConfigEntry<bool> UnloadUnusedAssets; internal static ConfigEntry<float> DelaySeconds; internal static ConfigEntry<bool> Reports; internal static ConfigEntry<int> MipmapLimit; private void Awake() { //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Expected O, but got Unknown //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Expected O, but got Unknown //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; UnloadAudio = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "UnloadIdleDecompressedAudio", true, "When the ship is back in orbit, free the decompressed sample data of Decompress-On-Load clips that are not playing. Unity reloads a clip the next time it is played."); MinClipSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "MinClipSeconds", 3f, "Only unload clips at least this long. Short clips cost little and reloading them often is wasteful."); IncludePreloaded = ((BaseUnityPlugin)this).Config.Bind<bool>("Audio", "IncludePreloadedClips", false, "Also unload clips marked 'Preload Audio Data'. Frees more, but those clips may stutter once when first replayed."); SweepSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "SweepSeconds", 30f, "Also free idle Decompress-On-Load clips every this many seconds, not just in orbit. 0 = orbit only. Only the cheap audio pass runs this often; the full unused-asset unload freezes the game for a moment, so it stays in orbit."); IdleMinutes = ((BaseUnityPlugin)this).Config.Bind<float>("Audio", "IdleMinutes", 2f, "In the periodic sweep, only free clips that haven't been heard playing for this long, so sounds in use don't reload."); UnloadUnusedAssets = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "UnloadUnusedAssets", true, "Run Resources.UnloadUnusedAssets and a GC after the audio pass, to drop anything left behind by the last moon."); DelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("General", "DelaySeconds", 4f, "Seconds to wait after reaching orbit before cleaning, so the moon scene has finished unloading."); Reports = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "MemoryReports", false, "Write a memory breakdown to the log at the main menu, after each orbit cleanup and 40 s after each landing."); MipmapLimit = ((BaseUnityPlugin)this).Config.Bind<int>("Textures", "MipmapLimit", 1, new ConfigDescription("Skip this many of the largest mip levels on every mipmapped texture. Lethal Company renders the world at 620x364, so 1 (half resolution, about 75% less texture memory) looks the same; 2 saves more. 0 = off.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 3), Array.Empty<object>())); ApplyMipmapLimit(); GameObject val = new GameObject("MemoryJanitorRunner"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; Host = val.AddComponent<Runner>(); new Harmony("lcmemfix.MemoryJanitor").PatchAll(typeof(Patches)); Log.LogInfo((object)"MemoryJanitor loaded"); } internal static void ApplyMipmapLimit() { if (MipmapLimit.Value > 0 && QualitySettings.globalTextureMipmapLimit != MipmapLimit.Value) { QualitySettings.globalTextureMipmapLimit = MipmapLimit.Value; Log.LogInfo((object)$"Texture mipmap limit set to {MipmapLimit.Value}"); } } } internal class Runner : MonoBehaviour { private static readonly HashSet<string> NotMoons = new HashSet<string> { "SampleSceneRelay", "InitScene", "InitSceneLaunchOptions", "InitSceneLANMode", "MainMenu", "ColdOpen1", "CompanyBuilding" }; private bool menuReported; private float nextSweep; private readonly Dictionary<int, float> lastHeard = new Dictionary<int, float>(); private void Update() { float value = Plugin.SweepSeconds.Value; if (!(value <= 0f) && Plugin.UnloadAudio.Value && !(Time.realtimeSinceStartup < nextSweep)) { nextSweep = Time.realtimeSinceStartup + value; Sweep(); } } private void Sweep() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Invalid comparison between Unknown and I4 float realtimeSinceStartup = Time.realtimeSinceStartup; float num = Plugin.IdleMinutes.Value * 60f; AudioSource[] array = Object.FindObjectsOfType<AudioSource>(); foreach (AudioSource val in array) { if ((Object)(object)val.clip != (Object)null && val.isPlaying) { lastHeard[((Object)val.clip).GetInstanceID()] = realtimeSinceStartup; } } int num2 = 0; long num3 = 0L; AudioClip[] array2 = Resources.FindObjectsOfTypeAll<AudioClip>(); foreach (AudioClip val2 in array2) { if ((Object)(object)val2 == (Object)null || (int)val2.loadType != 0 || (int)val2.loadState != 2 || val2.length < Plugin.MinClipSeconds.Value || (val2.preloadAudioData && !Plugin.IncludePreloaded.Value)) { continue; } int instanceID = ((Object)val2).GetInstanceID(); if (!lastHeard.TryGetValue(instanceID, out var value)) { lastHeard[instanceID] = realtimeSinceStartup; } else if (!(realtimeSinceStartup - value < num)) { long num4 = (long)val2.samples * (long)val2.channels * 2; if (val2.UnloadAudioData()) { num2++; num3 += num4; lastHeard.Remove(instanceID); } } } if (num2 > 0) { Plugin.Log.LogInfo((object)$"Sweep: freed {num2} idle audio clips (~{num3 / 1048576} MB)"); } } private void OnEnable() { SceneManager.sceneLoaded += OnSceneLoaded; } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Invalid comparison between Unknown and I4 Plugin.ApplyMipmapLimit(); if (!Plugin.Reports.Value) { return; } if (((Scene)(ref scene)).name == "MainMenu") { if (!menuReported) { menuReported = true; ((MonoBehaviour)this).StartCoroutine(ReportAfter(20f, "main menu")); } } else if (!NotMoons.Contains(((Scene)(ref scene)).name) && (int)mode == 1) { ((MonoBehaviour)this).StartCoroutine(ReportAfter(40f, "landed on " + ((Scene)(ref scene)).name)); } } private IEnumerator ReportAfter(float seconds, string label) { yield return (object)new WaitForSecondsRealtime(seconds); Report.Write(label); } internal IEnumerator CleanAfterRound() { yield return (object)new WaitForSeconds(Plugin.DelaySeconds.Value); long before = Report.PrivateBytes(); int clips = 0; long freed = 0L; if (Plugin.UnloadAudio.Value) { HashSet<AudioClip> hashSet = new HashSet<AudioClip>(); AudioSource[] array = Resources.FindObjectsOfTypeAll<AudioSource>(); foreach (AudioSource val in array) { if ((Object)(object)val != (Object)null && (Object)(object)val.clip != (Object)null && val.isPlaying) { hashSet.Add(val.clip); } } AudioClip[] array2 = Resources.FindObjectsOfTypeAll<AudioClip>(); foreach (AudioClip val2 in array2) { if (!((Object)(object)val2 == (Object)null) && !hashSet.Contains(val2) && (int)val2.loadType == 0 && (int)val2.loadState == 2 && !(val2.length < Plugin.MinClipSeconds.Value) && (!val2.preloadAudioData || Plugin.IncludePreloaded.Value)) { long num = (long)val2.samples * (long)val2.channels * 2; if (val2.UnloadAudioData()) { clips++; freed += num; } } } } if (Plugin.UnloadUnusedAssets.Value) { yield return Resources.UnloadUnusedAssets(); GC.Collect(); } long num2 = Report.PrivateBytes(); Plugin.Log.LogInfo((object)($"Orbit cleanup: unloaded {clips} idle audio clips (~{freed / 1048576} MB of samples). " + $"Private bytes {before / 1048576} MB -> {num2 / 1048576} MB")); if (Plugin.Reports.Value) { Report.Write("orbit"); } } } internal static class Report { private struct ProcessMemoryCounters { public uint cb; public uint PageFaultCount; public ulong PeakWorkingSetSize; public ulong WorkingSetSize; public ulong QuotaPeakPagedPoolUsage; public ulong QuotaPagedPoolUsage; public ulong QuotaPeakNonPagedPoolUsage; public ulong QuotaNonPagedPoolUsage; public ulong PagefileUsage; public ulong PeakPagefileUsage; public ulong PrivateUsage; } internal const long MB = 1048576L; [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", EntryPoint = "K32GetProcessMemoryInfo")] private static extern bool GetProcessMemoryInfo(IntPtr process, out ProcessMemoryCounters counters, uint size); internal static long PrivateBytes() { try { ProcessMemoryCounters counters = new ProcessMemoryCounters { cb = (uint)Marshal.SizeOf<ProcessMemoryCounters>() }; return (long)(GetProcessMemoryInfo(GetCurrentProcess(), out counters, counters.cb) ? counters.PrivateUsage : 0); } catch { return 0L; } } internal static void Write(string label) { Plugin.Log.LogInfo((object)($"[Report:{label}] private={PrivateBytes() / 1048576} MB, unity allocated={Profiler.GetTotalAllocatedMemoryLong() / 1048576} MB, " + $"reserved={Profiler.GetTotalReservedMemoryLong() / 1048576} MB, mono={Profiler.GetMonoUsedSizeLong() / 1048576} MB, " + $"gfx={Profiler.GetAllocatedMemoryForGraphicsDriver() / 1048576} MB, mip limit={QualitySettings.globalTextureMipmapLimit}")); int num = 0; foreach (AssetBundle allLoadedAssetBundle in AssetBundle.GetAllLoadedAssetBundles()) { _ = allLoadedAssetBundle; num++; } Plugin.Log.LogInfo((object)$"[Report:{label}] loaded asset bundles: {num}"); Type<Texture2D>(label, (Func<Texture2D, string>)((Texture2D t) => ((Texture)t).width + "x" + ((Texture)t).height + " " + ((object)t.format/*cast due to .constrained prefix*/).ToString() + (((Texture)t).isReadable ? " readable" : "")), (Func<Texture2D, long>)TextureBytes); Type<Cubemap>(label, (Func<Cubemap, string>)((Cubemap t) => ((Texture)t).width + " " + ((object)t.format/*cast due to .constrained prefix*/).ToString()), (Func<Cubemap, long>)TextureBytes); Type<Texture3D>(label, (Func<Texture3D, string>)((Texture3D t) => ((Texture)t).width + "x" + ((Texture)t).height + "x" + t.depth), (Func<Texture3D, long>)null); Type<Texture2DArray>(label, (Func<Texture2DArray, string>)((Texture2DArray t) => ((Texture)t).width + "x" + ((Texture)t).height + "x" + t.depth), (Func<Texture2DArray, long>)null); Type<RenderTexture>(label, (Func<RenderTexture, string>)((RenderTexture t) => ((Texture)t).width + "x" + ((Texture)t).height + " " + ((object)t.graphicsFormat/*cast due to .constrained prefix*/).ToString()), (Func<RenderTexture, long>)null); Type<AudioClip>(label, (Func<AudioClip, string>)((AudioClip c) => $"{c.length:F0}s {c.loadType} {c.loadState}"), (Func<AudioClip, long>)AudioBytes); Type<Mesh>(label, (Func<Mesh, string>)((Mesh m) => m.vertexCount + " verts" + (m.isReadable ? " readable" : "")), (Func<Mesh, long>)null); Type<Shader>(label, (Func<Shader, string>)((Shader s) => ""), (Func<Shader, long>)null); Type<Material>(label, (Func<Material, string>)((Material m) => ""), (Func<Material, long>)null); Type<AnimationClip>(label, (Func<AnimationClip, string>)((AnimationClip a) => ""), (Func<AnimationClip, long>)null); Type<Font>(label, (Func<Font, string>)((Font f) => ""), (Func<Font, long>)null); Type<GameObject>(label, (Func<GameObject, string>)((GameObject g) => ""), (Func<GameObject, long>)null); } private static long TextureBytes(Texture t) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) long num = 0L; int width = t.width; int height = t.height; int num2 = Mathf.Max(1, t.mipmapCount); int num3 = ((!(t is Cubemap)) ? 1 : 6); for (int i = 0; i < num2; i++) { num += GraphicsFormatUtility.ComputeMipmapSize(Mathf.Max(1, width >> i), Mathf.Max(1, height >> i), t.graphicsFormat); } return num * num3; } private static long AudioBytes(AudioClip c) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_000d: Unknown result type (might be due to invalid IL or missing references) if ((int)c.loadState != 2) { return 0L; } if ((int)c.loadType != 0) { return 0L; } return (long)c.samples * (long)c.channels * 2; } private static void Type<T>(string label, Func<T, string> describe, Func<T, long> estimate) where T : Object { T[] array = Resources.FindObjectsOfTypeAll<T>(); long num = 0L; List<KeyValuePair<long, T>> list = new List<KeyValuePair<long, T>>(array.Length); T[] array2 = array; foreach (T val in array2) { long num2 = Profiler.GetRuntimeMemorySizeLong((Object)(object)val); if (num2 == 0L && estimate != null) { num2 = estimate(val); } num += num2; list.Add(new KeyValuePair<long, T>(num2, val)); } list.Sort((KeyValuePair<long, T> a, KeyValuePair<long, T> b) => b.Key.CompareTo(a.Key)); Plugin.Log.LogInfo((object)$"[Report:{label}] {typeof(T).Name}: {array.Length} objects, {num / 1048576} MB"); for (int num3 = 0; num3 < list.Count && num3 < 10 && list[num3].Key > 0; num3++) { Plugin.Log.LogInfo((object)$"[Report:{label}] {(double)list[num3].Key / 1048576.0:F1} MB {((Object)list[num3].Value).name} {describe(list[num3].Value)}"); } } } internal static class Patches { [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "SetShipReadyToLand")] private static void AfterReachingOrbit() { ((MonoBehaviour)Plugin.Host).StartCoroutine(Plugin.Host.CleanAfterRound()); } } }