Decompiled source of OTCLoader v1.0.5

Plugins/OverTheCounter-Loader.dll

Decompiled 2 days ago
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using MelonLoader;
using MelonLoader.Utils;
using Mono.Cecil;
using Mono.Collections.Generic;
using Newtonsoft.Json;
using OverTheCounter.Loader;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(LoaderPlugin), "OTC Loader", "1.0.5", "hdlmrell", null)]
[assembly: MelonColor(100, 200, 180, 255)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("0.0.0.0")]
namespace OverTheCounter.Loader;

internal enum Branch
{
	Mono,
	Il2Cpp
}
[Serializable]
internal class LoaderConfig
{
	public string[] Whitelist = Array.Empty<string>();
}
public class LoaderPlugin : MelonPlugin
{
	private const string DisabledExt = ".off";

	private const string ConfigFileName = "OTCLoader.config.json";

	private static readonly string[] BuiltinBlacklist = new string[2] { "OverTheCounter-Loader.dll", "SwapperPlugin.dll" };

	private const uint MB_OKCANCEL = 1u;

	private const uint MB_YESNO = 4u;

	private const uint MB_ICONQUESTION = 32u;

	private const uint MB_ICONWARNING = 48u;

	private const uint MB_ICONINFORMATION = 64u;

	private const int IDOK = 1;

	private const int IDCANCEL = 2;

	private const int IDYES = 6;

	private const int IDNO = 7;

	private static readonly Instance Logger = new Instance("OTC Loader");

	private static LoaderConfig _config = new LoaderConfig();

	private static string _configPath = "";

	[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
	private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);

	public override void OnPreInitialization()
	{
		if (AppDomain.CurrentDomain.GetData("OTC_LOADER_INITIALIZED") != null)
		{
			Logger.Msg("Another OTC Loader instance already ran — skipping this copy.");
			return;
		}
		AppDomain.CurrentDomain.SetData("OTC_LOADER_INITIALIZED", true);
		string modsDirectory = MelonEnvironment.ModsDirectory;
		if (!Directory.Exists(modsDirectory))
		{
			return;
		}
		LoadConfig();
		Branch branch = (MelonUtils.IsGameIl2Cpp() ? Branch.Il2Cpp : Branch.Mono);
		string text = ((branch == Branch.Il2Cpp) ? "IL2CPP" : "Mono");
		string text2 = ((branch == Branch.Il2Cpp) ? "Mono" : "IL2CPP");
		string[] array = new string[256];
		int num = 0;
		string[] files = Directory.GetFiles(modsDirectory, "*", SearchOption.AllDirectories);
		foreach (string text3 in files)
		{
			bool flag = text3.EndsWith(".dll.off", StringComparison.OrdinalIgnoreCase);
			bool flag2 = text3.EndsWith(".dll.off.di", StringComparison.OrdinalIgnoreCase);
			bool flag3 = text3.EndsWith(".dll.di", StringComparison.OrdinalIgnoreCase);
			if (!flag && !flag2 && !flag3)
			{
				continue;
			}
			string text4 = (flag2 ? ".off.di" : (flag3 ? ".di" : ".off"));
			string text5 = text3.Substring(0, text3.Length - text4.Length);
			try
			{
				if (!File.Exists(text5))
				{
					File.Move(text3, text5);
					if ((flag || flag2) && num < array.Length)
					{
						array[num++] = Path.GetFileName(text5);
					}
				}
				else
				{
					File.Delete(text3);
				}
			}
			catch (Exception ex)
			{
				Logger.Warning("Could not restore " + Path.GetFileName(text3) + ": " + ex.Message);
			}
		}
		string[] files2 = Directory.GetFiles(modsDirectory, "*.dll", SearchOption.AllDirectories);
		bool[] array2 = new bool[files2.Length];
		Branch?[] array3 = new Branch?[files2.Length];
		for (int j = 0; j < files2.Length; j++)
		{
			string fileName = Path.GetFileName(files2[j]);
			array2[j] = IsBlacklisted(fileName);
			if (!array2[j])
			{
				array3[j] = DetectBranch(files2[j]);
			}
		}
		for (int k = 0; k < files2.Length; k++)
		{
			if (array2[k] || !array3[k].HasValue)
			{
				continue;
			}
			string directoryName = Path.GetDirectoryName(files2[k]);
			if (!Path.GetFileName(directoryName).Equals("Plugins", StringComparison.OrdinalIgnoreCase))
			{
				continue;
			}
			string b = StripBranchKeyword(Path.GetFileName(files2[k]));
			bool flag4 = false;
			for (int l = 0; l < files2.Length; l++)
			{
				if (l != k && !array2[l] && string.Equals(Path.GetDirectoryName(files2[l]), directoryName, StringComparison.OrdinalIgnoreCase) && string.Equals(StripBranchKeyword(Path.GetFileName(files2[l])), b, StringComparison.OrdinalIgnoreCase))
				{
					flag4 = true;
					break;
				}
			}
			if (!flag4)
			{
				array2[k] = true;
			}
		}
		int num2 = 0;
		string[] array4 = new string[files2.Length];
		int num3 = 0;
		string[] array5 = new string[files2.Length];
		string[] array6 = new string[files2.Length];
		string[] array7 = new string[files2.Length];
		int num4 = 0;
		for (int m = 0; m < files2.Length; m++)
		{
			if (array2[m] || !array3[m].HasValue || array3[m] == branch)
			{
				continue;
			}
			string text6 = files2[m];
			string fileName2 = Path.GetFileName(text6);
			string directoryName2 = Path.GetDirectoryName(text6);
			bool flag5 = false;
			try
			{
				File.Move(text6, text6 + ".off");
				num2++;
				for (int n = 0; n < num; n++)
				{
					if (string.Equals(array[n], fileName2, StringComparison.OrdinalIgnoreCase))
					{
						flag5 = true;
						break;
					}
				}
				if (!flag5)
				{
					Logger.Msg("Disabled '" + fileName2 + "' — targets " + text2 + " but game is " + text + ".");
					if (num3 < array4.Length)
					{
						array4[num3++] = text6;
					}
				}
			}
			catch (Exception ex2)
			{
				Logger.Warning("Could not disable '" + fileName2 + "': " + ex2.Message);
				continue;
			}
			bool flag6 = string.Equals(directoryName2, modsDirectory, StringComparison.OrdinalIgnoreCase);
			bool flag7 = false;
			for (int num5 = 0; num5 < num4; num5++)
			{
				if (!flag6 && string.Equals(array5[num5], directoryName2, StringComparison.OrdinalIgnoreCase))
				{
					flag7 = true;
					break;
				}
			}
			if (flag7)
			{
				continue;
			}
			bool flag8 = false;
			string b2 = StripBranchKeyword(fileName2);
			for (int num6 = 0; num6 < files2.Length; num6++)
			{
				if (num6 != m && !array2[num6] && (!array3[num6].HasValue || array3[num6] == branch))
				{
					bool num7 = !flag6 && string.Equals(Path.GetDirectoryName(files2[num6]), directoryName2, StringComparison.OrdinalIgnoreCase);
					bool flag9 = string.Equals(StripBranchKeyword(Path.GetFileName(files2[num6])), b2, StringComparison.OrdinalIgnoreCase);
					if (num7 || flag9)
					{
						flag8 = true;
						break;
					}
				}
			}
			if (flag8)
			{
				if (flag5)
				{
					continue;
				}
				string text7 = "";
				for (int num8 = 0; num8 < files2.Length; num8++)
				{
					if (num8 != m && !array2[num8] && (!array3[num8].HasValue || array3[num8] == branch))
					{
						bool num9 = !flag6 && string.Equals(Path.GetDirectoryName(files2[num8]), directoryName2, StringComparison.OrdinalIgnoreCase);
						bool flag10 = string.Equals(StripBranchKeyword(Path.GetFileName(files2[num8])), b2, StringComparison.OrdinalIgnoreCase);
						if (num9 || flag10)
						{
							text7 = Path.GetFileName(files2[num8]);
							break;
						}
					}
				}
				Logger.Msg("  → Compatible version kept: " + text7);
				continue;
			}
			string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName2);
			string text8 = "";
			for (int num10 = 0; num10 < files2.Length; num10++)
			{
				if (!array2[num10] && array3[num10].HasValue && array3[num10] != branch && string.Equals(Path.GetDirectoryName(files2[num10]), directoryName2, StringComparison.OrdinalIgnoreCase) && (!flag6 || string.Equals(fileName2, Path.GetFileName(files2[num10]), StringComparison.OrdinalIgnoreCase)))
				{
					if (text8.Length > 0)
					{
						text8 += ", ";
					}
					text8 += Path.GetFileName(files2[num10]);
				}
			}
			array5[num4] = directoryName2;
			array6[num4] = fileNameWithoutExtension;
			array7[num4] = text8;
			num4++;
		}
		if (num4 > 0)
		{
			Logger.Warning("╔══════════════════════════════════════════════════════════╗");
			Logger.Warning("║  INCOMPATIBLE MODS — no " + text + "-compatible version found");
			Logger.Warning("║");
			for (int num11 = 0; num11 < num4; num11++)
			{
				Logger.Warning("║  • " + array6[num11]);
				Logger.Warning("║    Disabled: " + array7[num11]);
			}
			Logger.Warning("║");
			Logger.Warning("║  → Check each mod page for a " + text + "-compatible release,");
			Logger.Warning("║    or switch your game to the branch those mods support.");
			Logger.Warning("║");
			Logger.Warning("║  → Think this is a mistake? Open the config file and");
			Logger.Warning("║    add the filename to the Whitelist:");
			Logger.Warning("║    " + _configPath);
			Logger.Warning("╚══════════════════════════════════════════════════════════╝");
		}
		if (num2 > 0 || num4 > 0)
		{
			string text9 = "";
			for (int num12 = 0; num12 < num4; num12++)
			{
				if (text9.Length > 0)
				{
					text9 += ", ";
				}
				text9 += array6[num12];
			}
			string text10 = ((num4 > 0) ? (": " + text9) : "");
			Logger.Msg("Done — " + num2 + " wrong-branch DLL(s) correctly handled, " + num4 + " mod(s) have no compatible version" + text10 + ".");
		}
		else
		{
			Logger.Msg("All DLLs are compatible with " + text + ".");
		}
		if (num3 > 0)
		{
			InteractiveFlow(array4, num3, branch, files2, array2, array3, modsDirectory);
		}
	}

	private static void InteractiveFlow(string[] firstTimePaths, int count, Branch gameBranch, string[] allDlls, bool[] skip, Branch?[] branches, string modsPath)
	{
		string text = ((gameBranch == Branch.Il2Cpp) ? "IL2CPP" : "Mono");
		string text2 = ((gameBranch == Branch.Il2Cpp) ? "Mono" : "IL2CPP");
		string[] array = new string[count];
		int num = 0;
		for (int i = 0; i < count; i++)
		{
			string text3 = firstTimePaths[i];
			string? fileName = Path.GetFileName(text3);
			string directoryName = Path.GetDirectoryName(text3);
			string b = StripBranchKeyword(fileName);
			bool flag = false;
			for (int j = 0; j < allDlls.Length; j++)
			{
				if (j != i && !skip[j] && (!branches[j].HasValue || branches[j] == gameBranch))
				{
					bool num2 = !string.Equals(directoryName, modsPath, StringComparison.OrdinalIgnoreCase) && string.Equals(Path.GetDirectoryName(allDlls[j]), directoryName, StringComparison.OrdinalIgnoreCase);
					bool flag2 = string.Equals(StripBranchKeyword(Path.GetFileName(allDlls[j])), b, StringComparison.OrdinalIgnoreCase);
					if (num2 || flag2)
					{
						flag = true;
						break;
					}
				}
			}
			if (!flag)
			{
				array[num++] = text3;
			}
		}
		bool flag3 = false;
		try
		{
			if (_config.Whitelist != null && _config.Whitelist.Length != 0)
			{
				string text4 = "OTC Loader — Whitelist Management\n\nYour whitelist contains " + _config.Whitelist.Length + " mod(s).\nKeep the current whitelist or clear it entirely?\n\n[OK]     —  (Recommended) Keep current whitelist\n[Cancel] —  Clear whitelist";
				if (MessageBox(IntPtr.Zero, text4, "OTC Loader", 33u) == 2)
				{
					_config.Whitelist = Array.Empty<string>();
					flag3 = true;
					Logger.Msg("User chose to clear the whitelist.");
				}
			}
			bool flag4 = false;
			if (num > 0)
			{
				string text5 = "";
				for (int k = 0; k < num; k++)
				{
					if (k > 0)
					{
						text5 += "\n";
					}
					text5 = text5 + "• " + Path.GetFileName(array[k]);
					if (k >= 9)
					{
						text5 = text5 + "\n...and " + (num - 10) + " more.";
						break;
					}
				}
				string text6 = "OTC Loader — Incompatible Mods Detected\n\nThese mods target " + text2 + " but your game runs " + text + ".\nNo compatible versions were found, so they were disabled:\n\n" + text5 + "\n\nKeep all of them disabled, or review each mod to optionally whitelist them?\n\n[OK]     —  (Recommended) Keep all disabled\n[Cancel] —  Review each mod";
				flag4 = MessageBox(IntPtr.Zero, text6, "OTC Loader", 49u) == 2;
			}
			if (flag4)
			{
				for (int l = 0; l < num; l++)
				{
					string fileName2 = Path.GetFileName(array[l]);
					string text7 = "OTC Loader — Review Mod\n\n" + fileName2 + "\n\nThis mod targets " + text2 + " but your game runs " + text + ".\n\nKeep this mod disabled, or add it to the whitelist? (May cause crashes if whitelisted)\n\n[OK]     —  (Recommended) Keep disabled\n[Cancel] —  Whitelist this mod";
					if (MessageBox(IntPtr.Zero, text7, "OTC Loader", 33u) == 2)
					{
						string[] array2 = _config.Whitelist ?? Array.Empty<string>();
						string[] array3 = new string[array2.Length + 1];
						Array.Copy(array2, array3, array2.Length);
						array3[array2.Length] = fileName2;
						_config.Whitelist = array3;
						flag3 = true;
						Logger.Msg("User whitelisted: " + fileName2);
					}
				}
			}
			if (flag3)
			{
				SaveConfig();
			}
		}
		catch (Exception ex)
		{
			Logger.Warning("Interactive prompt failed (falling back to log-only): " + ex.Message);
		}
		string text8 = "";
		for (int m = 0; m < count; m++)
		{
			if (text8.Length > 0)
			{
				text8 += ", ";
			}
			text8 += Path.GetFileNameWithoutExtension(firstTimePaths[m]);
		}
		Logger.Warning("First-time disable of: " + text8);
		Logger.Warning("A restart is recommended so the disabled DLLs are fully unloaded.");
		string text9 = "SAFE TO RUN! Your compatible mods will work fine.\n\nOTC Loader safely disabled these incompatible mod files:\n" + text8 + "\n\nThe runtime may have already cached the old files. A restart is recommended.\n\n[OK]     —  (Recommended) Close game NOW. Restart via your mod manager.\n[Cancel] —  Continue anyway (may cause errors)";
		try
		{
			if (MessageBox(IntPtr.Zero, text9, "OTC Loader — Restart Recommended", 65u) == 1)
			{
				Environment.Exit(0);
			}
		}
		catch
		{
			Logger.Warning("╔══════════════════════════════════════════════════════════╗");
			Logger.Warning("║  RESTART RECOMMENDED                                     ║");
			Logger.Warning("║                                                          ║");
			Logger.Warning("║  Incompatible mods were disabled but may have already    ║");
			Logger.Warning("║  been cached. Please close and restart the game.         ║");
			Logger.Warning("║  Affected: " + text8);
			Logger.Warning("╚══════════════════════════════════════════════════════════╝");
		}
	}

	private static void LoadConfig()
	{
		_configPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "OTCLoader.config.json");
		string configPath = _configPath;
		if (File.Exists(configPath))
		{
			try
			{
				_config = JsonConvert.DeserializeObject<LoaderConfig>(File.ReadAllText(configPath)) ?? new LoaderConfig();
				return;
			}
			catch (Exception ex)
			{
				Logger.Warning("Could not read config, using defaults: " + ex.Message);
				_config = new LoaderConfig();
				return;
			}
		}
		_config = new LoaderConfig();
		try
		{
			File.WriteAllText(configPath, "{\n  \"_readme\": \"Add DLL filenames to Whitelist to prevent OTC Loader from disabling them.\",\n  \"Whitelist\": [\n    \"ExampleMod-IL2Cpp.dll\",\n    \"AnotherExampleMod-IL2Cpp.dll\"\n  ]\n}\n");
			Logger.Msg("Created default config at " + configPath);
		}
		catch
		{
		}
	}

	private static void SaveConfig()
	{
		if (string.IsNullOrEmpty(_configPath))
		{
			return;
		}
		try
		{
			string contents = JsonConvert.SerializeObject((object)_config, (Formatting)1);
			File.WriteAllText(_configPath, contents);
		}
		catch (Exception ex)
		{
			Logger.Warning("Could not save config: " + ex.Message);
		}
	}

	private static string StripBranchKeyword(string filename)
	{
		string text = Path.GetFileNameWithoutExtension(filename).ToLowerInvariant();
		string[] array = new string[6] { "-il2cpp", "_il2cpp", ".il2cpp", "-mono", "_mono", ".mono" };
		foreach (string text2 in array)
		{
			if (text.EndsWith(text2))
			{
				return text.Substring(0, text.Length - text2.Length);
			}
		}
		return text;
	}

	private static bool IsBlacklisted(string filename)
	{
		if (!Array.Exists(BuiltinBlacklist, (string b) => string.Equals(b, filename, StringComparison.OrdinalIgnoreCase)) && !filename.StartsWith("S1API", StringComparison.OrdinalIgnoreCase))
		{
			return Array.Exists(_config.Whitelist ?? Array.Empty<string>(), (string w) => string.Equals(w, filename, StringComparison.OrdinalIgnoreCase));
		}
		return true;
	}

	private static Branch? DetectBranch(string path)
	{
		//IL_0042: Unknown result type (might be due to invalid IL or missing references)
		//IL_0047: Unknown result type (might be due to invalid IL or missing references)
		string text = Path.GetFileName(path).ToLowerInvariant();
		if (text.Contains("mono"))
		{
			return Branch.Mono;
		}
		if (text.Contains("il2cpp"))
		{
			return Branch.Il2Cpp;
		}
		try
		{
			AssemblyDefinition val = AssemblyDefinition.ReadAssembly(path);
			try
			{
				Enumerator<ModuleDefinition> enumerator = val.Modules.GetEnumerator();
				try
				{
					while (enumerator.MoveNext())
					{
						foreach (TypeReference typeReference in enumerator.Current.GetTypeReferences())
						{
							string text2 = (typeReference.Namespace ?? "").ToLowerInvariant();
							if (text2.StartsWith("il2cppscheduleone") || text2.StartsWith("il2cppsystem"))
							{
								return Branch.Il2Cpp;
							}
							if (text2 == "scheduleone")
							{
								return Branch.Mono;
							}
						}
					}
				}
				finally
				{
					((IDisposable)enumerator).Dispose();
				}
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}
		catch
		{
		}
		return null;
	}
}