imnyang/BlockModLoader
Archived
Watch
2
0
Fork
You've already forked BlockModLoader
0

Release BlockModLoader 1.0.0 with loader whitelist and recommendations

This commit is contained in:
암냥 2026-10-05 14:02:51 +09:00
commit 43a421d224
4 changed files with 301 additions and 89 deletions

View file

@ -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.");
}
}

View 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>

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

View file

@ -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)