Release BlockModLoader 1.0.0 with loader whitelist and recommendations
This commit is contained in:
parent
d5d9fbf47c
commit
43a421d224
8 changed files with 301 additions and 89 deletions
31
BlockModLoader.csproj
Normal file
31
BlockModLoader.csproj
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)/$(MSBuildToolsVersion)/Microsoft.Common.props" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
|
||||||
|
<ProjectGuid>{173C8DD2-70E0-4CA2-BC2B-9B839CE464A9}</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType><TargetFrameworkVersion>v4.8</TargetFrameworkVersion><LangVersion>latest</LangVersion>
|
||||||
|
<AssemblyName>BlockModLoader</AssemblyName><RootNamespace>BlockModLoader</RootNamespace>
|
||||||
|
<PackageId>BlockModLoader</PackageId><Version>1.0.0</Version>
|
||||||
|
<Authors>imnyang</Authors><Description>Blocks mod initialization under MelonLoader, BepInEx, or Unity Mod Manager unless explicitly whitelisted.</Description>
|
||||||
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
|
<PackageProjectUrl>https://git.mizuki.guru/imnyang/BlockModLoader</PackageProjectUrl>
|
||||||
|
<RepositoryUrl>https://git.mizuki.guru/imnyang/BlockModLoader.git</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageOutputPath>$(MSBuildProjectDirectory)/artifacts</PackageOutputPath>
|
||||||
|
<RestorePackagesPath>$(MSBuildProjectDirectory)/packages/nuget</RestorePackagesPath>
|
||||||
|
<OutputPath>bin/$(Configuration)/</OutputPath><Optimize>true</Optimize>
|
||||||
|
<TargetFrameworkRootPath Condition="Exists('packages/net48/build/.NETFramework/v4.8')">$(MSBuildProjectDirectory)/packages/net48/build/</TargetFrameworkRootPath>
|
||||||
|
<FrameworkPathOverride Condition="Exists('packages/net48/build/.NETFramework/v4.8')">$(MSBuildProjectDirectory)/packages/net48/build/.NETFramework/v4.8</FrameworkPathOverride>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="NuGet.Build.Tasks.Pack" Version="6.11.1" PrivateAssets="all" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||||
|
<None Include="LICENSE.md" Pack="true" PackagePath="/" />
|
||||||
|
<Reference Include="mscorlib"><Private>false</Private></Reference>
|
||||||
|
<Reference Include="System"><Private>false</Private></Reference>
|
||||||
|
<Reference Include="System.Core"><Private>false</Private></Reference>
|
||||||
|
<Compile Include="LoaderGuard.cs"/>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)/Microsoft.CSharp.targets"/>
|
||||||
|
</Project>
|
||||||
|
|
@ -4,25 +4,56 @@ using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
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 class LoaderGuard
|
||||||
{
|
{
|
||||||
public static void EnsureAllowed(string modName)
|
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<Assembly> assemblies,
|
internal static void EnsureAllowed(string modName, IEnumerable<Assembly> assemblies,
|
||||||
Action<string> showAlert, Action<string> terminate)
|
Action<string> showAlert, Action<string> terminate, string recommendedLoader = null,
|
||||||
|
WhiteListLoader whiteListLoader = WhiteListLoader.None)
|
||||||
{
|
{
|
||||||
|
if ((whiteListLoader & ~WhiteListLoader.All) != 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(whiteListLoader));
|
||||||
|
|
||||||
foreach (var assembly in assemblies)
|
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;
|
continue;
|
||||||
|
|
||||||
var message = modName + " cannot run while MelonLoader is active.\n\n"
|
var loaderName = loader == WhiteListLoader.UMM ? "Unity Mod Manager (UMM)" : loader.ToString();
|
||||||
+ "Remove MelonLoader and install this mod using Unity Mod Manager.\n\n"
|
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.";
|
+ "The game will close after this message.";
|
||||||
// Loader exception handlers can swallow a thrown exception, so termination
|
// Loader exception handlers can swallow a thrown exception, so termination
|
||||||
// must happen even if the native dialog fails.
|
// 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)
|
private static void ShowAlert(string message)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(message);
|
Console.Error.WriteLine(message);
|
||||||
|
|
|
||||||
111
README.md
111
README.md
|
|
@ -1,36 +1,96 @@
|
||||||
# BlockMelonLoader
|
# BlockModLoader
|
||||||
|
|
||||||
A .NET Framework 4.8 library with no Unity or MelonLoader dependency.
|
A .NET Framework 4.8 library with no Unity or mod loader dependency.
|
||||||
Call `BlockMelonLoader.LoaderGuard.EnsureAllowed("YourMod")` before initializing
|
Call `BlockModLoader.LoaderGuard.EnsureAllowed("YourMod")` before initializing
|
||||||
a mod. Building this library does not require a game installation.
|
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
|
By default, the guard blocks any of these loaded assemblies in the current AppDomain
|
||||||
shows a Windows error dialog and calls `Environment.FailFast` after dismissal.
|
(exact names, case-insensitive):
|
||||||
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.
|
|
||||||
|
|
||||||
Add a `PackageReference` dependency on `BlockMelonLoader` version `1.0.0`.
|
| Loader | Assembly names |
|
||||||
Ship `BlockMelonLoader.dll` alongside your mod DLL; NuGet copies the runtime asset
|
| --- | --- |
|
||||||
to the consuming project's build output.
|
| MelonLoader | `MelonLoader` |
|
||||||
|
| BepInEx | `BepInEx`, `BepInEx.Core` |
|
||||||
|
| Unity Mod Manager (UMM) | `UnityModManager` |
|
||||||
|
|
||||||
This is an initialization guard, not a MelonLoader plugin. Simply placing the
|
The BepInEx assembly names follow the [BepInEx 5 API](https://docs.bepinex.dev/api/index.html)
|
||||||
UMM-only mod DLL in MelonLoader's Mods folder will not invoke the guard.
|
and [BepInEx 6 API](https://docs.bepinex.dev/master/api/index.html); UMM uses
|
||||||
A bridge that invokes the UMM entry point is checked; direct assembly loading
|
[`UnityModManager`](https://github.com/newman55/unity-mod-manager/blob/master/UnityModManager/UnityModManager.csproj).
|
||||||
without invoking that entry point is not. Modified binaries can bypass this check.
|
|
||||||
|
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
|
## Build and test
|
||||||
|
|
||||||
On Windows, run `./build.ps1 -Test`. Visual Studio or Rider MSBuild is required;
|
On Windows, run `./build.ps1 -Test`. Visual Studio or Rider MSBuild is required;
|
||||||
use `-MSBuildPath` to select it explicitly. The script downloads .NET Framework
|
use `-MSBuildPath` to select it explicitly. The script downloads .NET Framework
|
||||||
4.8 reference assemblies into the ignored `packages` folder when needed.
|
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
|
The DLL is written to `bin/Release/BlockModLoader.dll` and the NuGet package to
|
||||||
`artifacts/BlockMelonLoader.1.0.0.nupkg`. Packaging uses `NuGet.Build.Tasks.Pack`.
|
`artifacts/BlockModLoader.1.0.0.nupkg`. Packaging uses `NuGet.Build.Tasks.Pack`.
|
||||||
|
|
||||||
The standalone tests check detection, notification ordering, and dialog failure
|
The standalone tests check every loader name, case-insensitive detection,
|
||||||
handling. They substitute the dialog and termination callbacks and do not crash
|
unrelated assemblies, multiple loaders, notification ordering, and dialog failure
|
||||||
the game. Run them directly with `Tests/bin/Release/BlockMelonLoader.Tests.exe`.
|
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
|
## Installation
|
||||||
|
|
||||||
|
|
@ -40,16 +100,17 @@ The NuGet feed for this project is:
|
||||||
https://git.mizuki.guru/api/packages/imnyang/nuget/index.json
|
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
|
```xml
|
||||||
<PackageReference Include="BlockMelonLoader" Version="[1.0.0]" />
|
<PackageReference Include="BlockModLoader" Version="[1.0.0]" />
|
||||||
```
|
```
|
||||||
|
|
||||||
For local development, build this repository and use the generated `.nupkg` in
|
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.
|
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
|
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
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<string> alert = message =>
|
|
||||||
{
|
|
||||||
if (!message.Contains("SampleMod") || !message.Contains("Unity Mod Manager"))
|
|
||||||
throw new Exception("Missing mod name or installation guidance.");
|
|
||||||
alerts++;
|
|
||||||
};
|
|
||||||
Action<string> 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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
Tests/BlockModLoader.Tests.csproj
Normal file
18
Tests/BlockModLoader.Tests.csproj
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)/$(MSBuildToolsVersion)/Microsoft.Common.props"/>
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType><AssemblyName>BlockModLoader.Tests</AssemblyName><TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
|
<LangVersion>latest</LangVersion><OutputPath>bin/Release/</OutputPath>
|
||||||
|
<TargetFrameworkRootPath Condition="Exists('../packages/net48/build/.NETFramework/v4.8')">$(MSBuildProjectDirectory)/../packages/net48/build/</TargetFrameworkRootPath>
|
||||||
|
<FrameworkPathOverride Condition="Exists('../packages/net48/build/.NETFramework/v4.8')">$(MSBuildProjectDirectory)/../packages/net48/build/.NETFramework/v4.8</FrameworkPathOverride>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="mscorlib"><Private>false</Private></Reference>
|
||||||
|
<Reference Include="System"><Private>false</Private></Reference>
|
||||||
|
<Reference Include="System.Core"><Private>false</Private></Reference>
|
||||||
|
<Compile Include="Program.cs"/>
|
||||||
|
<Compile Include="BlockModLoaderTests.cs"/>
|
||||||
|
<Compile Include="../LoaderGuard.cs" Link="LoaderGuard.cs"/>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)/Microsoft.CSharp.targets"/>
|
||||||
|
</Project>
|
||||||
152
Tests/BlockModLoaderTests.cs
Normal file
152
Tests/BlockModLoaderTests.cs
Normal file
|
|
@ -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<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,8 +6,8 @@ internal static class Program
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
BlockMelonLoaderTests.Run();
|
BlockModLoaderTests.Run();
|
||||||
Console.WriteLine("BlockMelonLoader tests passed.");
|
Console.WriteLine("BlockModLoader tests passed.");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|
|
||||||
10
build.ps1
10
build.ps1
|
|
@ -28,15 +28,15 @@ try {
|
||||||
Remove-Item -LiteralPath $archive
|
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 ($LASTEXITCODE -ne 0) { throw 'Library build failed.' }
|
||||||
if ($Test) {
|
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.' }
|
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.' }
|
if ($LASTEXITCODE -ne 0) { throw 'Tests failed.' }
|
||||||
}
|
}
|
||||||
Write-Host "Library output: $PSScriptRoot/bin/Release/BlockMelonLoader.dll"
|
Write-Host "Library output: $PSScriptRoot/bin/Release/BlockModLoader.dll"
|
||||||
Write-Host "NuGet output: $PSScriptRoot/artifacts/BlockMelonLoader.1.0.0.nupkg"
|
Write-Host "NuGet output: $PSScriptRoot/artifacts/BlockModLoader.1.0.0.nupkg"
|
||||||
}
|
}
|
||||||
finally { Pop-Location }
|
finally { Pop-Location }
|
||||||
|
|
|
||||||
Reference in a new issue