Decompiled source of SmoothShip v0.1.0

SmoothShip.dll

Decompiled an hour ago
using 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));
			}
		}
	}
}