diff --git a/BlockMelonLoader.csproj b/BlockModLoader.csproj
similarity index 81%
rename from BlockMelonLoader.csproj
rename to BlockModLoader.csproj
index d93d55a..cf5f3fc 100644
--- a/BlockMelonLoader.csproj
+++ b/BlockModLoader.csproj
@@ -4,12 +4,12 @@
Debug
{173C8DD2-70E0-4CA2-BC2B-9B839CE464A9}
Libraryv4.8latest
- BlockMelonLoaderBlockMelonLoader
- BlockMelonLoader1.0.0
- imnyangBlocks mod initialization when MelonLoader is active.
+ BlockModLoaderBlockModLoader
+ BlockModLoader1.0.0
+ imnyangBlocks mod initialization under MelonLoader, BepInEx, or Unity Mod Manager unless explicitly whitelisted.
GPL-3.0-only
- https://git.mizuki.guru/imnyang/BlockMelonLoader
- https://git.mizuki.guru/imnyang/BlockMelonLoader.git
+ https://git.mizuki.guru/imnyang/BlockModLoader
+ https://git.mizuki.guru/imnyang/BlockModLoader.git
git
README.md
$(MSBuildProjectDirectory)/artifacts
diff --git a/LoaderGuard.cs b/LoaderGuard.cs
index 1bc941a..761303c 100644
--- a/LoaderGuard.cs
+++ b/LoaderGuard.cs
@@ -4,25 +4,56 @@ using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
-namespace BlockMelonLoader
+namespace BlockModLoader
{
+ [Flags]
+ public enum WhiteListLoader
+ {
+ None = 0,
+ MelonLoader = 1,
+ BepInEx = 2,
+ UMM = 4,
+ All = MelonLoader | BepInEx | UMM
+ }
+
public static class LoaderGuard
{
public static void EnsureAllowed(string modName)
{
- EnsureAllowed(modName, AppDomain.CurrentDomain.GetAssemblies(), ShowAlert, Environment.FailFast);
+ EnsureAllowed(modName, null);
+ }
+
+ public static void EnsureAllowed(string modName, string recommendedLoader)
+ {
+ EnsureAllowed(modName, WhiteListLoader.None, recommendedLoader);
+ }
+
+ public static void EnsureAllowed(string modName, WhiteListLoader whiteListLoader,
+ string recommendedLoader = null)
+ {
+ EnsureAllowed(modName, AppDomain.CurrentDomain.GetAssemblies(), ShowAlert, Environment.FailFast,
+ recommendedLoader, whiteListLoader);
}
internal static void EnsureAllowed(string modName, IEnumerable assemblies,
- Action showAlert, Action terminate)
+ Action showAlert, Action terminate, string recommendedLoader = null,
+ WhiteListLoader whiteListLoader = WhiteListLoader.None)
{
+ if ((whiteListLoader & ~WhiteListLoader.All) != 0)
+ throw new ArgumentOutOfRangeException(nameof(whiteListLoader));
+
foreach (var assembly in assemblies)
{
- if (!string.Equals(assembly.GetName().Name, "MelonLoader", StringComparison.OrdinalIgnoreCase))
+ var loader = GetLoader(assembly.GetName().Name);
+ if (loader == WhiteListLoader.None || (whiteListLoader & loader) != 0)
continue;
- var message = modName + " cannot run while MelonLoader is active.\n\n"
- + "Remove MelonLoader and install this mod using Unity Mod Manager.\n\n"
+ var loaderName = loader == WhiteListLoader.UMM ? "Unity Mod Manager (UMM)" : loader.ToString();
+ var message = modName + " cannot run while " + loaderName + " is active.\n\n"
+ + "Remove the unsupported mod loader and follow this mod's installation instructions.\n\n"
+ + (string.IsNullOrWhiteSpace(recommendedLoader)
+ ? string.Empty
+ : "Recommended mod loader: " + recommendedLoader.Trim() + ".\n\n")
+ "The game will close after this message.";
// Loader exception handlers can swallow a thrown exception, so termination
// must happen even if the native dialog fails.
@@ -32,6 +63,18 @@ namespace BlockMelonLoader
}
}
+ private static WhiteListLoader GetLoader(string assemblyName)
+ {
+ if (string.Equals(assemblyName, "MelonLoader", StringComparison.OrdinalIgnoreCase))
+ return WhiteListLoader.MelonLoader;
+ if (string.Equals(assemblyName, "BepInEx", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(assemblyName, "BepInEx.Core", StringComparison.OrdinalIgnoreCase))
+ return WhiteListLoader.BepInEx;
+ if (string.Equals(assemblyName, "UnityModManager", StringComparison.OrdinalIgnoreCase))
+ return WhiteListLoader.UMM;
+ return WhiteListLoader.None;
+ }
+
private static void ShowAlert(string message)
{
Console.Error.WriteLine(message);
diff --git a/README.md b/README.md
index 8e45723..c4b6199 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,96 @@
-# BlockMelonLoader
+# BlockModLoader
-A .NET Framework 4.8 library with no Unity or MelonLoader dependency.
-Call `BlockMelonLoader.LoaderGuard.EnsureAllowed("YourMod")` before initializing
-a mod. Building this library does not require a game installation.
+A .NET Framework 4.8 library with no Unity or mod loader dependency.
+Call `BlockModLoader.LoaderGuard.EnsureAllowed("YourMod")` before initializing
+a mod or game integration. Building this library does not require a game installation.
-If an assembly named `MelonLoader` is loaded in the current AppDomain, the guard
-shows a Windows error dialog and calls `Environment.FailFast` after dismissal.
-Termination also happens if showing the dialog fails. On other platforms the
-message is written to standard error before termination; there is no native dialog.
-The guard does not inspect installation folders, modify loader files, or monitor
-assemblies loaded after the check. UMM and MelonLoader running together are blocked.
+By default, the guard blocks any of these loaded assemblies in the current AppDomain
+(exact names, case-insensitive):
-Add a `PackageReference` dependency on `BlockMelonLoader` version `1.0.0`.
-Ship `BlockMelonLoader.dll` alongside your mod DLL; NuGet copies the runtime asset
-to the consuming project's build output.
+| Loader | Assembly names |
+| --- | --- |
+| MelonLoader | `MelonLoader` |
+| BepInEx | `BepInEx`, `BepInEx.Core` |
+| Unity Mod Manager (UMM) | `UnityModManager` |
-This is an initialization guard, not a MelonLoader plugin. Simply placing the
-UMM-only mod DLL in MelonLoader's Mods folder will not invoke the guard.
-A bridge that invokes the UMM entry point is checked; direct assembly loading
-without invoking that entry point is not. Modified binaries can bypass this check.
+The BepInEx assembly names follow the [BepInEx 5 API](https://docs.bepinex.dev/api/index.html)
+and [BepInEx 6 API](https://docs.bepinex.dev/master/api/index.html); UMM uses
+[`UnityModManager`](https://github.com/newman55/unity-mod-manager/blob/master/UnityModManager/UnityModManager.csproj).
+
+When a non-whitelisted supported loader is detected, the guard names it in a Windows error
+dialog and calls `Environment.FailFast` after dismissal, terminating the entire
+game process. Termination also happens if showing the dialog fails. On other
+platforms the message is written to standard error before termination.
+When multiple loaders are present, the first detected non-whitelisted loader triggers
+one dialog and termination. Without a whitelist, all three loaders are blocked.
+
+The guard checks only assemblies already loaded when `EnsureAllowed` is called.
+It does not inspect installation folders, modify loader files, or monitor later
+assembly loads. Names such as `MelonLoader.Helper` or `BepInEx.Helper` are not
+matched. Renamed or modified loader binaries can bypass the check.
+
+This is an initialization guard. Merely placing its DLL in a game or loader
+folder will not invoke it: the consuming mod or game must call `EnsureAllowed`.
+
+## Allowed loaders
+
+Use `WhiteListLoader` to allow selected loaders for each guard call:
+
+```csharp
+using BlockModLoader;
+
+// Allow BepInEx; block MelonLoader and UMM.
+LoaderGuard.EnsureAllowed("YourMod", whiteListLoader: WhiteListLoader.BepInEx,
+ recommendedLoader: "BepInEx");
+
+// Allow both BepInEx and UMM; block MelonLoader.
+LoaderGuard.EnsureAllowed("YourMod", WhiteListLoader.BepInEx | WhiteListLoader.UMM);
+```
+
+Available values are `None`, `MelonLoader`, `BepInEx`, `UMM`, and `All`.
+Combine values with `|`. `None` (the default for existing calls) blocks all three;
+`All` allows all three. `BepInEx` allows both `BepInEx` and `BepInEx.Core`.
+An allowed loader does not prevent detection of another blocked loader in the
+same AppDomain. Unknown flag values throw `ArgumentOutOfRangeException`.
+The whitelist applies only to these three known loaders; other assemblies are
+still ignored. Each call supplies its own whitelist, with no shared global setting.
+
+## Recommended loader in the alert
+
+Pass an optional display name as the second argument to include a recommendation
+in the alert:
+
+```csharp
+BlockModLoader.LoaderGuard.EnsureAllowed("YourMod", recommendedLoader: "YourPreferredLoader");
+```
+
+The alert adds `Recommended mod loader: YourPreferredLoader.` before the game
+closure notice. Omit the second argument, or pass null, an empty string, or
+whitespace, to keep the original generic guidance. Leading and trailing
+whitespace is trimmed. This setting changes only the alert text; it does not
+allow the recommended loader or change which loaders are blocked.
+
+## Migration from BlockMelonLoader
+
+Replace the `BlockMelonLoader` package reference with `BlockModLoader` version
+`1.0.0`, update imports to `using BlockModLoader;`, and ship `BlockModLoader.dll`
+alongside your mod DLL. NuGet copies the runtime asset to the consuming project's
+build output. The `LoaderGuard.EnsureAllowed("YourMod")` method remains the same,
+but now blocks BepInEx and UMM as well as MelonLoader.
## Build and test
On Windows, run `./build.ps1 -Test`. Visual Studio or Rider MSBuild is required;
use `-MSBuildPath` to select it explicitly. The script downloads .NET Framework
4.8 reference assemblies into the ignored `packages` folder when needed.
-The DLL is written to `bin/Release/BlockMelonLoader.dll` and the NuGet package to
-`artifacts/BlockMelonLoader.1.0.0.nupkg`. Packaging uses `NuGet.Build.Tasks.Pack`.
+The DLL is written to `bin/Release/BlockModLoader.dll` and the NuGet package to
+`artifacts/BlockModLoader.1.0.0.nupkg`. Packaging uses `NuGet.Build.Tasks.Pack`.
-The standalone tests check detection, notification ordering, and dialog failure
-handling. They substitute the dialog and termination callbacks and do not crash
-the game. Run them directly with `Tests/bin/Release/BlockMelonLoader.Tests.exe`.
+The standalone tests check every loader name, case-insensitive detection,
+unrelated assemblies, multiple loaders, notification ordering, and dialog failure
+handling, plus every whitelist combination and invalid flags.
+They substitute the dialog and termination callbacks and do not crash
+the game. Run them directly with `Tests/bin/Release/BlockModLoader.Tests.exe`.
## Installation
@@ -40,16 +100,17 @@ The NuGet feed for this project is:
https://git.mizuki.guru/api/packages/imnyang/nuget/index.json
```
-Add that feed to the consuming project's `NuGet.Config`, then add the dependency:
+After the new package has been published to that feed, add it to the consuming
+project's `NuGet.Config`, then add the dependency:
```xml
-
+
```
For local development, build this repository and use the generated `.nupkg` in
a local NuGet feed. The source repository is not required when building a consumer.
New library versions must use a new package version and update the consumer's
-dependency version.
+dependency version. The source repository is https://git.mizuki.guru/imnyang/BlockModLoader.
## License
diff --git a/Tests/BlockMelonLoaderTests.cs b/Tests/BlockMelonLoaderTests.cs
deleted file mode 100644
index 69995d5..0000000
--- a/Tests/BlockMelonLoaderTests.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.Reflection;
-using System.Reflection.Emit;
-using BlockMelonLoader;
-
-internal static class BlockMelonLoaderTests
-{
- internal static void Run()
- {
- var alerts = 0;
- var terminations = 0;
- Action alert = message =>
- {
- if (!message.Contains("SampleMod") || !message.Contains("Unity Mod Manager"))
- throw new Exception("Missing mod name or installation guidance.");
- alerts++;
- };
- Action terminate = message =>
- {
- if (alerts == 0) throw new Exception("Termination preceded notification.");
- terminations++;
- };
-
- var unrelated = AppDomain.CurrentDomain.DefineDynamicAssembly(
- new AssemblyName("MelonLoader.Helper"), AssemblyBuilderAccess.Run);
- LoaderGuard.EnsureAllowed("SampleMod", new[] { typeof(LoaderGuard).Assembly, unrelated }, alert, terminate);
- if (alerts != 0 || terminations != 0) throw new Exception("Blocked an unrelated assembly.");
-
- var loader = AppDomain.CurrentDomain.DefineDynamicAssembly(
- new AssemblyName("melonloader"), AssemblyBuilderAccess.Run);
- LoaderGuard.EnsureAllowed("SampleMod", new[] { loader }, alert, terminate);
- if (alerts != 1 || terminations != 1) throw new Exception("MelonLoader was not blocked.");
-
- try
- {
- LoaderGuard.EnsureAllowed("SampleMod", new[] { loader },
- message => { throw new InvalidOperationException("Dialog unavailable."); },
- message => terminations++);
- throw new Exception("Expected the simulated dialog failure.");
- }
- catch (InvalidOperationException) { }
- if (terminations != 2) throw new Exception("Dialog failure bypassed termination.");
- }
-}
diff --git a/Tests/BlockMelonLoader.Tests.csproj b/Tests/BlockModLoader.Tests.csproj
similarity index 85%
rename from Tests/BlockMelonLoader.Tests.csproj
rename to Tests/BlockModLoader.Tests.csproj
index 26b654d..2927190 100644
--- a/Tests/BlockMelonLoader.Tests.csproj
+++ b/Tests/BlockModLoader.Tests.csproj
@@ -1,7 +1,7 @@
- ExeBlockMelonLoader.Testsv4.8
+ ExeBlockModLoader.Testsv4.8
latestbin/Release/
$(MSBuildProjectDirectory)/../packages/net48/build/
$(MSBuildProjectDirectory)/../packages/net48/build/.NETFramework/v4.8
@@ -11,7 +11,7 @@
false
false
-
+
diff --git a/Tests/BlockModLoaderTests.cs b/Tests/BlockModLoaderTests.cs
new file mode 100644
index 0000000..c40f165
--- /dev/null
+++ b/Tests/BlockModLoaderTests.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Reflection;
+using System.Reflection.Emit;
+using BlockModLoader;
+
+internal static class BlockModLoaderTests
+{
+ internal static void Run()
+ {
+ var unrelated = new[]
+ {
+ typeof(LoaderGuard).Assembly, CreateAssembly("MelonLoader.Helper"),
+ CreateAssembly("BepInEx.Helper"), CreateAssembly("BepInEx.Core.Helper"),
+ CreateAssembly("UnityModManager.Helper"), CreateAssembly("Harmony")
+ };
+ Action unexpected = message => { throw new Exception("Blocked an unrelated assembly."); };
+ LoaderGuard.EnsureAllowed("SampleMod", new Assembly[0], unexpected, unexpected);
+ LoaderGuard.EnsureAllowed("SampleMod", unrelated, unexpected, unexpected);
+
+ CheckLoader("MelonLoader", "MelonLoader");
+ CheckLoader("melonloader", "MelonLoader");
+ CheckLoader("BepInEx", "BepInEx");
+ CheckLoader("bepinex", "BepInEx");
+ CheckLoader("BepInEx.Core", "BepInEx");
+ CheckLoader("bepinex.core", "BepInEx");
+ CheckLoader("UnityModManager", "Unity Mod Manager (UMM)");
+ CheckLoader("unitymodmanager", "Unity Mod Manager (UMM)");
+ CheckWhiteList();
+
+ foreach (var loaderName in new[] { "MelonLoader", "BepInEx", "BepInEx.Core", "UnityModManager" })
+ {
+ foreach (var recommendation in new[] { null, "", " \t ", "CustomLoader", " UMM " })
+ CheckRecommendation(loaderName, recommendation);
+ }
+ LoaderGuard.EnsureAllowed("SampleMod", unrelated, unexpected, unexpected, "CustomLoader");
+
+ var alerts = 0;
+ var terminations = 0;
+ LoaderGuard.EnsureAllowed("SampleMod", new[]
+ { unrelated[1], CreateAssembly("MelonLoader"), CreateAssembly("BepInEx.Core"), CreateAssembly("UnityModManager") },
+ message => alerts++, message => terminations++);
+ if (alerts != 1 || terminations != 1)
+ throw new Exception("Multiple loaders should trigger only one notification and termination.");
+ }
+
+ private static void CheckWhiteList()
+ {
+ var names = new[] { "melonloader", "bepinex", "bepinex.core", "unitymodmanager" };
+ var types = new[] { WhiteListLoader.MelonLoader, WhiteListLoader.BepInEx,
+ WhiteListLoader.BepInEx, WhiteListLoader.UMM };
+ for (var flags = 0; flags <= (int)WhiteListLoader.All; flags++)
+ {
+ var whiteList = (WhiteListLoader)flags;
+ for (var i = 0; i < names.Length; i++)
+ {
+ var alerts = 0;
+ var terminations = 0;
+ LoaderGuard.EnsureAllowed("SampleMod", new[] { CreateAssembly(names[i]) },
+ message => alerts++, message => terminations++, "CustomLoader", whiteList);
+ var expected = (whiteList & types[i]) != 0 ? 0 : 1;
+ if (alerts != expected || terminations != expected)
+ throw new Exception("Whitelist incorrectly handled " + names[i] + " with " + whiteList);
+ }
+
+ var mixedAlerts = 0;
+ var mixedTerminations = 0;
+ LoaderGuard.EnsureAllowed("SampleMod", new[] { CreateAssembly("BepInEx.Core"),
+ CreateAssembly("UnityModManager"), CreateAssembly("MelonLoader") },
+ message => mixedAlerts++, message => mixedTerminations++, null, whiteList);
+ var mixedExpected = whiteList == WhiteListLoader.All ? 0 : 1;
+ if (mixedAlerts != mixedExpected || mixedTerminations != mixedExpected)
+ throw new Exception("An allowed loader bypassed detection of a blocked loader.");
+ }
+
+ foreach (var invalid in new[] { (WhiteListLoader)8, (WhiteListLoader)(-1) })
+ {
+ try
+ {
+ LoaderGuard.EnsureAllowed("SampleMod", new Assembly[0],
+ message => { }, message => { }, null, invalid);
+ throw new Exception("Expected invalid whitelist flags to be rejected.");
+ }
+ catch (ArgumentOutOfRangeException) { }
+ }
+ }
+
+ private static void CheckRecommendation(string loaderName, string recommendation)
+ {
+ string alertMessage = null;
+ var terminations = 0;
+ LoaderGuard.EnsureAllowed("SampleMod", new[] { CreateAssembly(loaderName) },
+ message =>
+ {
+ alertMessage = message;
+ if (string.IsNullOrWhiteSpace(recommendation))
+ {
+ if (message.Contains("Recommended mod loader:"))
+ throw new Exception("An empty recommendation should not be displayed.");
+ }
+ else if (!message.Contains("Recommended mod loader: " + recommendation.Trim() + ".\n\n"))
+ throw new Exception("The configured recommendation was not displayed.");
+ },
+ message =>
+ {
+ if (alertMessage == null || message != alertMessage)
+ throw new Exception("Recommended loader message was lost before termination.");
+ terminations++;
+ }, recommendation);
+ if (terminations != 1)
+ throw new Exception("A recommendation changed loader blocking behavior.");
+ }
+
+ private static void CheckLoader(string assemblyName, string loaderName)
+ {
+ var loader = CreateAssembly(assemblyName);
+ var alerts = 0;
+ var terminations = 0;
+ string alertMessage = null;
+ LoaderGuard.EnsureAllowed("SampleMod", new[] { typeof(LoaderGuard).Assembly, loader },
+ message =>
+ {
+ if (!message.Contains("SampleMod cannot run while " + loaderName + " is active.")
+ || !message.Contains("follow this mod's installation instructions.")
+ || !message.Contains("The game will close"))
+ throw new Exception("Missing mod name, detected loader, or installation guidance.");
+ alertMessage = message;
+ alerts++;
+ },
+ message =>
+ {
+ if (alerts != 1 || message != alertMessage)
+ throw new Exception("Termination preceded notification or used a different message.");
+ terminations++;
+ });
+ if (alerts != 1 || terminations != 1) throw new Exception(assemblyName + " was not blocked.");
+
+ try
+ {
+ LoaderGuard.EnsureAllowed("SampleMod", new[] { loader },
+ message => { throw new InvalidOperationException("Dialog unavailable."); },
+ message => terminations++);
+ throw new Exception("Expected the simulated dialog failure.");
+ }
+ catch (InvalidOperationException) { }
+ if (terminations != 2) throw new Exception("Dialog failure bypassed termination for " + assemblyName);
+ }
+
+ private static Assembly CreateAssembly(string name)
+ {
+ return AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run);
+ }
+}
diff --git a/Tests/Program.cs b/Tests/Program.cs
index 5fdd3a0..da86b0e 100644
--- a/Tests/Program.cs
+++ b/Tests/Program.cs
@@ -6,8 +6,8 @@ internal static class Program
{
try
{
- BlockMelonLoaderTests.Run();
- Console.WriteLine("BlockMelonLoader tests passed.");
+ BlockModLoaderTests.Run();
+ Console.WriteLine("BlockModLoader tests passed.");
return 0;
}
catch (Exception exception)
diff --git a/build.ps1 b/build.ps1
index 988ed71..86a89f6 100644
--- a/build.ps1
+++ b/build.ps1
@@ -28,15 +28,15 @@ try {
Remove-Item -LiteralPath $archive
}
- & $MSBuildPath BlockMelonLoader.csproj /restore '/t:Rebuild,Pack' /p:Configuration=Release /v:minimal /nologo
+ & $MSBuildPath BlockModLoader.csproj /restore '/t:Rebuild,Pack' /p:Configuration=Release /v:minimal /nologo
if ($LASTEXITCODE -ne 0) { throw 'Library build failed.' }
if ($Test) {
- & $MSBuildPath Tests/BlockMelonLoader.Tests.csproj /t:Rebuild /v:minimal /nologo
+ & $MSBuildPath Tests/BlockModLoader.Tests.csproj /t:Rebuild /v:minimal /nologo
if ($LASTEXITCODE -ne 0) { throw 'Test build failed.' }
- & ./Tests/bin/Release/BlockMelonLoader.Tests.exe
+ & ./Tests/bin/Release/BlockModLoader.Tests.exe
if ($LASTEXITCODE -ne 0) { throw 'Tests failed.' }
}
- Write-Host "Library output: $PSScriptRoot/bin/Release/BlockMelonLoader.dll"
- Write-Host "NuGet output: $PSScriptRoot/artifacts/BlockMelonLoader.1.0.0.nupkg"
+ Write-Host "Library output: $PSScriptRoot/bin/Release/BlockModLoader.dll"
+ Write-Host "NuGet output: $PSScriptRoot/artifacts/BlockModLoader.1.0.0.nupkg"
}
finally { Pop-Location }