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
8 changed files with 301 additions and 89 deletions

31
BlockModLoader.csproj Normal file
View 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>

View file

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

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

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

View file

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