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 SmoothShip v0.1.0
SmoothShip.dll
Decompiled an hour agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [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("SmoothShip")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+9a7aba15550bdff43785a251b821600d0538624d")] [assembly: AssemblyProduct("SmoothShip")] [assembly: AssemblyTitle("SmoothShip")] [assembly: AssemblyVersion("0.1.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 SmoothShip { internal static class Cleanup { internal static bool Active; private static float _due = -1f; private static Coroutine _routine; private static int _requests; internal static void Apply(Harmony h) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Expected O, but got Unknown //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Expected O, but got Unknown Type typeFromHandle = typeof(Cleanup); Type type = AccessTools.TypeByName("LethalLevelLoader.AssetBundles.AssetBundleLoader"); MethodInfo methodInfo = ((type != null) ? AccessTools.Method(type, "ClearCache", (Type[])null, (Type[])null) : null); if (methodInfo != null && methodInfo.IsStatic && methodInfo.GetParameters().Length == 0) { try { h.Patch((MethodBase)methodInfo, new HarmonyMethod(typeFromHandle, "LllClearPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active = true; Plugin.Log.LogInfo((object)"Merged cleanups: LethalLevelLoader's bundle-unload cleanup now runs once per batch of unloads."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Merged cleanups: could not patch LethalLevelLoader (" + ex.Message + ")")); } } else if (type != null) { Plugin.Log.LogWarning((object)"Merged cleanups: LethalLevelLoader's ClearCache isn't what Smooth Ship expects; left as it is."); } Type type2 = AccessTools.TypeByName("Dawn.Internal.DawnDungeonNetworker"); MethodInfo methodInfo2 = ((type2 != null) ? AccessTools.Method(type2, "UnloadExisting", (Type[])null, (Type[])null) : null); MethodInfo methodInfo3 = ((methodInfo2 != null) ? AccessTools.EnumeratorMoveNext((MethodBase)methodInfo2) : null); if (methodInfo3 != null) { try { h.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeFromHandle, "DropGcCollect", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); Active = true; return; } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Merged cleanups: could not patch DawnLib (" + ex2.Message + ")")); return; } } if (type2 != null) { Plugin.Log.LogWarning((object)"Merged cleanups: DawnLib's UnloadExisting isn't what Smooth Ship expects; left as it is."); } } private static bool LllClearPrefix() { Plugin instance = Plugin.Instance; if ((Object)(object)instance == (Object)null || !((Behaviour)instance).isActiveAndEnabled) { return true; } _requests++; _due = Time.realtimeSinceStartup + Plugin.CleanupDelay.Value; if (_routine == null) { _routine = ((MonoBehaviour)instance).StartCoroutine(Merged()); } return false; } private static IEnumerator Merged() { while (Time.realtimeSinceStartup < _due) { yield return null; } int n = _requests; _requests = 0; Stopwatch sw = Stopwatch.StartNew(); yield return Resources.UnloadUnusedAssets(); try { Caching.ClearCache(); } catch { } _routine = null; if (Plugin.LogTimes.Value) { Plugin.Log.LogInfo((object)$"Merged cleanup: one asset unload for {n} LethalLevelLoader request(s) ({sw.Elapsed.TotalMilliseconds:F0} ms incl. waiting for it to finish)"); } } private static void SkippedCollect() { } private static IEnumerable<CodeInstruction> DropGcCollect(IEnumerable<CodeInstruction> code) { MethodInfo collect = AccessTools.Method(typeof(GC), "Collect", Type.EmptyTypes, (Type[])null); int dropped = 0; foreach (CodeInstruction item in code) { if ((item.opcode == OpCodes.Call || item.opcode == OpCodes.Callvirt) && item.operand is MethodInfo methodInfo && methodInfo == collect) { dropped++; item.opcode = OpCodes.Call; item.operand = AccessTools.Method(typeof(Cleanup), "SkippedCollect", (Type[])null, (Type[])null); } yield return item; } Plugin.Log.LogInfo((object)((dropped > 0) ? "Merged cleanups: DawnLib's extra forced collection after unloading an interior removed." : "Merged cleanups: DawnLib has no forced collection to remove.")); } } public static class PluginInfo { public const string Guid = "daniel.lethalcompany.smoothship"; public const string Name = "SmoothShip"; public const string Version = "0.1.0"; } [BepInPlugin("daniel.lethalcompany.smoothship", "SmoothShip", "0.1.0")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; internal static ConfigEntry<bool> BatchSaves; internal static ConfigEntry<bool> VerifySaves; internal static ConfigEntry<bool> MergeCleanups; internal static ConfigEntry<bool> LogTimes; internal static ConfigEntry<float> CleanupDelay; internal static Plugin Instance; private readonly Harmony _harmony = new Harmony("daniel.lethalcompany.smoothship"); private void Awake() { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; BatchSaves = ((BaseUnityPlugin)this).Config.Bind<bool>("Saving", "BatchSaves", true, "Write the save file once per save. The game and mods otherwise read, decrypt, encrypt and rewrite the whole file for every value they save."); VerifySaves = ((BaseUnityPlugin)this).Config.Bind<bool>("Saving", "VerifySaves", false, "After each batched save, read the file back and check every value matches (slower; for testing)."); MergeCleanups = ((BaseUnityPlugin)this).Config.Bind<bool>("Cleanup", "MergeCleanups", true, "Replace LethalLevelLoader's cleanup (two asset unloads and a forced full garbage collection each time a bundle unloads) with one asset unload, and drop DawnLib's extra forced collection after it unloads an interior. Unity's asset unload already collects garbage."); CleanupDelay = ((BaseUnityPlugin)this).Config.Bind<float>("Cleanup", "CleanupDelay", 2f, new ConfigDescription("Seconds to wait for more bundles to unload before the one cleanup runs.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>())); LogTimes = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogTimes", false, "Log how long each batched save and each cleanup took."); if (BatchSaves.Value) { SaveBatch.Apply(_harmony); } if (MergeCleanups.Value) { Cleanup.Apply(_harmony); } Log.LogInfo((object)("SmoothShip 0.1.0 loaded (batched saves " + (SaveBatch.Active ? "on" : "off") + ", merged cleanups " + (Cleanup.Active ? "on" : "off") + ")")); } } internal static class SaveBatch { internal static bool Active; private static int _depth; private static bool _batching; private static bool _flushing; private static bool _weCached; private static string _path; private static int _mainThread; private static int _redirected; private static readonly Stopwatch Clock = new Stopwatch(); private static FieldInfo _cacheField; private static FieldInfo _bytesField; private static bool OnMain => Thread.CurrentThread.ManagedThreadId == _mainThread; internal static void Apply(Harmony h) { //IL_01e7: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_0201: Unknown result type (might be due to invalid IL or missing references) //IL_020c: Expected O, but got Unknown //IL_020c: Expected O, but got Unknown //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Expected O, but got Unknown //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Expected O, but got Unknown //IL_024a: Unknown result type (might be due to invalid IL or missing references) //IL_0258: Expected O, but got Unknown //IL_0264: Unknown result type (might be due to invalid IL or missing references) //IL_0272: Expected O, but got Unknown //IL_027e: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Expected O, but got Unknown try { _mainThread = Thread.CurrentThread.ManagedThreadId; MethodInfo methodInfo = AccessTools.Method(typeof(GameNetworkManager), "SaveGameValues", (Type[])null, (Type[])null); Type typeFromHandle = typeof(ES3Settings); ConstructorInfo constructorInfo = AccessTools.Constructor(typeFromHandle, new Type[2] { typeof(string), typeof(ES3Settings) }, false); ConstructorInfo constructorInfo2 = AccessTools.Constructor(typeFromHandle, new Type[2] { typeof(string), typeof(Enum[]) }, false); ConstructorInfo constructorInfo3 = AccessTools.Constructor(typeof(ES3File), new Type[2] { typeof(ES3Settings), typeof(bool) }, false); MethodInfo methodInfo2 = AccessTools.Method(typeof(ES3), "DeleteFile", new Type[1] { typeof(ES3Settings) }, (Type[])null); Type type = AccessTools.TypeByName("ES3Internal.ES3Stream"); Type type2 = AccessTools.TypeByName("ES3Internal.ES3FileMode"); MethodInfo methodInfo3 = ((type != null && type2 != null) ? AccessTools.Method(type, "CreateStream", new Type[2] { typeof(ES3Settings), type2 }, (Type[])null) : null); if (methodInfo == null || constructorInfo == null || constructorInfo2 == null || constructorInfo3 == null || methodInfo2 == null || methodInfo3 == null) { Plugin.Log.LogWarning((object)"Batched saves off: this game version's save code isn't what Smooth Ship expects."); return; } _cacheField = AccessTools.Field(typeof(ES3File), "cache"); Type type3 = AccessTools.TypeByName("ES3Internal.ES3Data") ?? AccessTools.TypeByName("ES3Data"); _bytesField = ((type3 != null) ? AccessTools.Field(type3, "bytes") : null); Type typeFromHandle2 = typeof(SaveBatch); h.Patch((MethodBase)methodInfo, new HarmonyMethod(typeFromHandle2, "SavePrefix", (Type[])null) { priority = 800 }, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeFromHandle2, "SaveFinalizer", (Type[])null), (HarmonyMethod)null); h.Patch((MethodBase)constructorInfo, (HarmonyMethod)null, new HarmonyMethod(typeFromHandle2, "SettingsPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); h.Patch((MethodBase)constructorInfo2, (HarmonyMethod)null, new HarmonyMethod(typeFromHandle2, "SettingsPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); h.Patch((MethodBase)constructorInfo3, new HarmonyMethod(typeFromHandle2, "FileCtorPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); h.Patch((MethodBase)methodInfo2, new HarmonyMethod(typeFromHandle2, "DiskPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); h.Patch((MethodBase)methodInfo3, new HarmonyMethod(typeFromHandle2, "DiskPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Batched saves off: could not patch the save code (" + ex.Message + ")")); } } private static bool IsSaveFile(ES3Settings s) { if (s != null && _path != null) { return string.Equals(s.path, _path, StringComparison.OrdinalIgnoreCase); } return false; } private static void SavePrefix() { if (!Active || !OnMain || _depth++ > 0) { return; } GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || !instance.isHostingGame || string.IsNullOrEmpty(instance.currentSaveFileName)) { return; } try { Clock.Restart(); _path = instance.currentSaveFileName; _redirected = 0; if (ES3File.cachedFiles.ContainsKey(_path)) { _path = null; _weCached = false; return; } _weCached = true; if (ES3.FileExists(_path)) { ES3.CacheFile(_path); } _batching = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Batched saves: could not load the save file into memory, saving the normal way this time (" + ex.Message + ")")); if (_weCached && _path != null) { ES3File.cachedFiles.Remove(_path); } _batching = false; _path = null; } } private static Exception SaveFinalizer(Exception __exception) { if (!Active || !OnMain) { return __exception; } if (--_depth > 0) { return __exception; } _depth = 0; if (_path != null) { bool batching = _batching; Flush("save done"); if (Plugin.LogTimes.Value) { Plugin.Log.LogInfo((object)string.Format("Batched save: {0} save-file calls in memory, one write; {1:F0} ms{2}", _redirected, Clock.Elapsed.TotalMilliseconds, batching ? "" : " (batching stopped early this save)")); } _path = null; } return __exception; } private static void SettingsPostfix(ES3Settings __instance) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (_batching && !_flushing && OnMain && (int)__instance.location == 0 && IsSaveFile(__instance)) { __instance.location = (Location)4; _redirected++; } } private static void FileCtorPrefix(ES3Settings settings, bool syncWithFile) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Invalid comparison between Unknown and I4 if (_batching && !_flushing && syncWithFile && IsSaveFile(settings)) { if ((int)settings.location == 4) { settings.location = (Location)0; } Flush("an ES3File opened the save file"); } } private static void DiskPrefix(ES3Settings settings) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Invalid comparison between Unknown and I4 if (_batching && !_flushing && IsSaveFile(settings)) { if ((int)settings.location == 4) { settings.location = (Location)0; } Flush(OnMain ? "something opened the save file directly" : "another thread opened the save file"); } } private static void Flush(string why) { if (!_batching) { return; } _batching = false; _flushing = true; Dictionary<string, byte[]> dictionary = null; try { if (ES3File.cachedFiles.TryGetValue(_path, out var value)) { if (Plugin.VerifySaves.Value) { dictionary = Snapshot(value); } ES3.StoreCachedFile(_path); } if (why != "save done") { Plugin.Log.LogInfo((object)("Batched save: wrote the save file early because " + why + "; the rest of this save goes straight to disk.")); } } catch (Exception ex) { Active = false; Plugin.Log.LogError((object)("Batched saves: writing the save file failed, so batching is off for this session: " + ex)); } finally { if (_weCached) { ES3File.cachedFiles.Remove(_path); } _flushing = false; } if (dictionary != null) { Verify(dictionary); } } private static Dictionary<string, byte[]> Snapshot(ES3File f) { Dictionary<string, byte[]> dictionary = new Dictionary<string, byte[]>(); if (_cacheField?.GetValue(f) is IDictionary dictionary2) { foreach (DictionaryEntry item in dictionary2) { dictionary[(string)item.Key] = _bytesField?.GetValue(item.Value) as byte[]; } } return dictionary; } private static void Verify(Dictionary<string, byte[]> want) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown try { Dictionary<string, byte[]> disk = Snapshot(new ES3File(new ES3Settings(_path, (ES3Settings)null), true)); List<string> list = want.Keys.Where((string k) => !disk.ContainsKey(k)).ToList(); List<string> list2 = disk.Keys.Where((string k) => !want.ContainsKey(k)).ToList(); byte[] value; List<string> list3 = want.Keys.Where((string k) => disk.TryGetValue(k, out value) && !((ReadOnlySpan<byte>)(value ?? Array.Empty<byte>())).SequenceEqual((ReadOnlySpan<byte>)(want[k] ?? Array.Empty<byte>()))).ToList(); if (list.Count + list2.Count + list3.Count == 0) { Plugin.Log.LogInfo((object)$"Batched save verify PASS: {want.Count} keys on disk match the batched copy"); return; } Plugin.Log.LogError((object)("Batched save verify FAIL: missing [" + string.Join(", ", list) + "] extra [" + string.Join(", ", list2) + "] different [" + string.Join(", ", list3) + "]")); } catch (Exception ex) { Plugin.Log.LogError((object)("Batched save verify FAIL: could not read the file back: " + ex.Message)); } } } }