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;
}
}