5 Commits

Author SHA1 Message Date
  Meivyn 01a05403ef
Bump version 8 months ago
  Meivyn 60099afb8a
Improve documentation target 8 months ago
  Meivyn 6b9cbb3239
Move anti yeet to separate patch 8 months ago
  Meivyn 217347d4a6
Fix path encoding issue in proxy 9 months ago
  Meivyn 29431a0716
Implement async `OnEnable`/`OnDisable` 10 months ago
9 changed files with 525 additions and 490 deletions
Unified View
  1. +14
    -9
      Doorstop/Proxy/main.c
  2. +33
    -0
      IPA.Injector/AntiYeetPatch.cs
  3. +126
    -128
      IPA.Injector/IPA.Injector.csproj
  4. +302
    -332
      IPA.Injector/Injector.cs
  5. +1
    -1
      IPA.Loader/Config/SelfConfig.cs
  6. +11
    -4
      IPA.Loader/Loader/Composite/CompositeBSPlugin.cs
  7. +36
    -14
      IPA.Loader/Loader/PluginExecutor.cs
  8. +1
    -1
      IPA.Loader/Loader/manifest.json
  9. +1
    -1
      IPA/Program.cs

+ 14
- 9
Doorstop/Proxy/main.c View File

@ -2,19 +2,19 @@
* main.cpp -- The main "entry point" and the main logic of the DLL. * main.cpp -- The main "entry point" and the main logic of the DLL.
* *
* Here, we define and initialize struct Main that contains the main code of this DLL. * Here, we define and initialize struct Main that contains the main code of this DLL.
*
*
* The main procedure goes as follows: * The main procedure goes as follows:
* 1. The loader checks that PatchLoader.dll and mono.dll exist * 1. The loader checks that PatchLoader.dll and mono.dll exist
* 2. mono.dll is loaded into memory and some of its functions are looked up * 2. mono.dll is loaded into memory and some of its functions are looked up
* 3. mono_jit_init_version is hooked with the help of MinHook * 3. mono_jit_init_version is hooked with the help of MinHook
*
*
* Then, the loader waits until Unity creates its root domain for mono (which is done with mono_jit_init_version). * Then, the loader waits until Unity creates its root domain for mono (which is done with mono_jit_init_version).
*
*
* Inside mono_jit_init_version hook: * Inside mono_jit_init_version hook:
* 1. Call the original mono_jit_init_version to get the Unity root domain * 1. Call the original mono_jit_init_version to get the Unity root domain
* 2. Load PatchLoader.dll into the root domain * 2. Load PatchLoader.dll into the root domain
* 3. Find and invoke PatchLoader.Loader.Run() * 3. Find and invoke PatchLoader.Loader.Run()
*
*
* Rest of the work is done on the managed side. * Rest of the work is done on the managed side.
* *
*/ */
@ -117,7 +117,7 @@ void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_ve
{ {
LOG("Debugger was already initialized\n"); LOG("Debugger was already initialized\n");
} }
// Call the original mono_jit_init_version to initialize the Unity Root Domain // Call the original mono_jit_init_version to initialize the Unity Root Domain
if (debug) { if (debug) {
char* opts[1]; char* opts[1];
@ -139,9 +139,14 @@ void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_ve
mono_debug_domain_create(domain); mono_debug_domain_create(domain);
} }
size_t len = WideCharToMultiByte(CP_UTF8, 0, targetAssembly, -1, NULL, 0, NULL, NULL);
char *dll_path = memalloc(sizeof(char) * len);
WideCharToMultiByte(CP_UTF8, 0, targetAssembly, -1, dll_path, len, NULL, NULL);
DWORD len = GetFullPathName(targetAssembly, 0, NULL, NULL);
wchar_t *full_path = memalloc(sizeof(wchar_t) * len);
GetFullPathName(targetAssembly, len, full_path, NULL);
size_t path_len = WideCharToMultiByte(CP_UTF8, 0, full_path, -1, NULL, 0, NULL, NULL);
char *dll_path = memalloc(sizeof(char) * path_len);
WideCharToMultiByte(CP_UTF8, 0, full_path, -1, dll_path, path_len, NULL, NULL);
memfree(full_path);
LOG("Loading assembly: %s\n", dll_path); LOG("Loading assembly: %s\n", dll_path);
// Load our custom assembly into the domain // Load our custom assembly into the domain
@ -234,7 +239,7 @@ void *ownMonoJitInitVersion(const char *root_domain_name, const char *runtime_ve
void* monostr = mono_object_to_string(exception, &exception); void* monostr = mono_object_to_string(exception, &exception);
if (exception != NULL) if (exception != NULL)
LOG("An error occurred while invoking the injector, but the error could not be stringified.\n") LOG("An error occurred while invoking the injector, but the error could not be stringified.\n")
else
else
{ {
char* str = mono_string_to_utf8(monostr); char* str = mono_string_to_utf8(monostr);
LOG("An error occurred invoking the injector: %s\n", str); LOG("An error occurred invoking the injector: %s\n", str);


+ 33
- 0
IPA.Injector/AntiYeetPatch.cs View File

@ -0,0 +1,33 @@
using HarmonyLib;
using System;
namespace IPA.Injector
{
internal static class AntiYeetPatch
{
private static Harmony instance;
public static void Apply()
{
#if BeatSaber
Logging.Logger.Injector.Info("Applying anti-yeet patch");
try
{
instance = new Harmony("BSIPA Anti-Yeet");
var original = AccessTools.Method("IPAPluginsDirDeleter:Awake");
var prefix = new HarmonyMethod(AccessTools.Method(typeof(AntiYeetPatch), nameof(SuppressIPAPluginsDirDeleter)));
instance.Patch(original, prefix);
}
catch (Exception e)
{
Logging.Logger.Injector.Warn("Could not apply anti-yeet patch");
Logging.Logger.Injector.Warn(e);
}
#endif
}
private static bool SuppressIPAPluginsDirDeleter() => false;
}
}

+ 126
- 128
IPA.Injector/IPA.Injector.csproj View File

@ -1,128 +1,126 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Common.props" />
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<RootNamespace>IPA.Injector</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net472' ">true</BuildForBeatSaber>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
<DefineConstants>$(DefineConstants);NET4</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net35'">
<DefineConstants>$(DefineConstants);NET3</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(BuildForBeatSaber)' == 'true'">
<DefineConstants>$(DefineConstants);BeatSaber</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj" />
<ProjectReference Include="..\Net3-Proxy\Net3-Proxy.csproj" Condition=" '$(TargetFramework)' == 'net35' " />
</ItemGroup>
<ItemGroup>
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net472' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net4.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net35' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net3.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Content Include="..\Libs\I18N.Net4.dll">
<Link>Libraries\Mono\I18N.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\I18N.West.Net4.dll">
<Link>Libraries\Mono\I18N.West.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\Microsoft.CSharp.dll">
<Link>Libraries\Mono\Microsoft.CSharp.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.Net4.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\netstandard.dll">
<Link>Libraries\Mono\netstandard.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="..\Libs\thirdparty\*">
<Link>Libraries\Thirdparty\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
<Content Include="..\Libs\I18N.Net3.dll">
<Link>Libraries\Mono\I18N.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\I18N.West.Net3.dll">
<Link>Libraries\Mono\I18N.West.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.Net3.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="AsyncBridge" Version="0.3.1" />
<ProjectReference Include="..\SemVer\SemVer.csproj" />
</ItemGroup>
<Target Name="CopyDocumentation" BeforeTargets="Build">
<ItemGroup>
<ReferenceFiles Include="%(Reference.RelativeDir)%(Reference.Filename).xml" />
</ItemGroup>
<Message Text="Copying documentation" />
<Copy SourceFiles="@(ReferenceFiles)" DestinationFolder="$(OutputPath)Libs" Condition="Exists('%(RootDir)%(Directory)%(Filename)%(Extension)')" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Message Text="Relocating" Importance="normal" />
<ItemGroup>
<SystemFiles Include="$(OutputPath)IPA.Injector.*" />
<SystemFiles Include="$(OutputPath)IPA.Loader.*" />
<SystemFiles Include="$(OutputPath)AsyncBridge.*" />
<SystemFiles Include="$(OutputPath)System.*" />
<SystemFiles Include="$(OutputPath)Portable.System.*" />
<SystemFiles Include="$(OutputPath)Net3-Proxy.*" />
<SystemFiles Include="$(OutputPath)Libraries\Mono\**\*" />
<OldLibFiles Include="$(OutputPath)Libs\**\*" />
</ItemGroup>
<Move SourceFiles="@(SystemFiles)" DestinationFolder="$(OutputPath)Data\Managed" />
<RemoveDir Directories="$(OutputPath)Libraries\Mono" />
<Delete Files="@(OldLibFiles)" />
<RemoveDir Directories="$(OutputPath)Libs" />
<ItemGroup>
<LibFiles Include="$(OutputPath)**\*" Exclude="$(OutputPath)Data\**\*;$(OutputPath)Libs\**\*" />
</ItemGroup>
<Move SourceFiles="@(LibFiles)" DestinationFolder="$(OutputPath)Libs\" />
<RemoveDir Directories="$(OutputPath)Libraries\Included" />
<RemoveDir Directories="$(OutputPath)Libraries" />
</Target>
<Import Project="..\Common.targets" />
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Common.props" />
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<RootNamespace>IPA.Injector</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<BuildForBeatSaber Condition=" '$(BuildForBeatSaber)' == '' And '$(TargetFramework)' == 'net472' ">true</BuildForBeatSaber>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net472'">
<DefineConstants>$(DefineConstants);NET4</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net35'">
<DefineConstants>$(DefineConstants);NET3</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(BuildForBeatSaber)' == 'true'">
<DefineConstants>$(DefineConstants);BeatSaber</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj" />
<ProjectReference Include="..\Net3-Proxy\Net3-Proxy.csproj" Condition=" '$(TargetFramework)' == 'net35' " />
</ItemGroup>
<ItemGroup>
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net472' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net4.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="UnityEngine.CoreModule" Condition=" '$(TargetFramework)' == 'net35' ">
<HintPath>..\Refs\UnityEngine.CoreModule.Net3.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net472' ">
<Content Include="..\Libs\I18N.Net4.dll">
<Link>Libraries\Mono\I18N.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\I18N.West.Net4.dll">
<Link>Libraries\Mono\I18N.West.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\Microsoft.CSharp.dll">
<Link>Libraries\Mono\Microsoft.CSharp.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.Net4.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\netstandard.dll">
<Link>Libraries\Mono\netstandard.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="..\Libs\thirdparty\*">
<Link>Libraries\Thirdparty\%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
<Content Include="..\Libs\I18N.Net3.dll">
<Link>Libraries\Mono\I18N.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\I18N.West.Net3.dll">
<Link>Libraries\Mono\I18N.West.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.Net3.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
<PackageReference Include="AsyncBridge" Version="0.3.1" />
<ProjectReference Include="..\SemVer\SemVer.csproj" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Message Text="Copying documentation" Importance="high" />
<ItemGroup>
<XmlFiles Include="%(ReferenceCopyLocalPaths.RelativeDir)%(ReferenceCopyLocalPaths.Filename).xml" Condition="Exists('%(ReferenceCopyLocalPaths.RelativeDir)%(ReferenceCopyLocalPaths.Filename).xml')" />
</ItemGroup>
<Copy SourceFiles="@(XmlFiles)" DestinationFolder="$(OutputPath)" />
<Message Text="Relocating" Importance="normal" />
<ItemGroup>
<SystemFiles Include="$(OutputPath)IPA.Injector.*" />
<SystemFiles Include="$(OutputPath)IPA.Loader.*" />
<SystemFiles Include="$(OutputPath)AsyncBridge.*" />
<SystemFiles Include="$(OutputPath)System.*" />
<SystemFiles Include="$(OutputPath)Portable.System.*" />
<SystemFiles Include="$(OutputPath)Net3-Proxy.*" />
<SystemFiles Include="$(OutputPath)Libraries\Mono\**\*" />
<OldLibFiles Include="$(OutputPath)Libs\**\*" />
</ItemGroup>
<Move SourceFiles="@(SystemFiles)" DestinationFolder="$(OutputPath)Data\Managed" />
<RemoveDir Directories="$(OutputPath)Libraries\Mono" />
<Delete Files="@(OldLibFiles)" />
<RemoveDir Directories="$(OutputPath)Libs" />
<ItemGroup>
<LibFiles Include="$(OutputPath)**\*" Exclude="$(OutputPath)Data\**\*;$(OutputPath)Libs\**\*" />
</ItemGroup>
<Move SourceFiles="@(LibFiles)" DestinationFolder="$(OutputPath)Libs\" />
<RemoveDir Directories="$(OutputPath)Libraries\Included" />
<RemoveDir Directories="$(OutputPath)Libraries" />
</Target>
<Import Project="..\Common.targets" />
</Project>

+ 302
- 332
IPA.Injector/Injector.cs View File

@ -1,332 +1,302 @@
#nullable enable
using IPA.AntiMalware;
using IPA.Config;
using IPA.Injector.Backups;
using IPA.Loader;
using IPA.Logging;
using IPA.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;
using static IPA.Logging.Logger;
using MethodAttributes = Mono.Cecil.MethodAttributes;
#if NET3
using Net3_Proxy;
using Path = Net3_Proxy.Path;
using File = Net3_Proxy.File;
using Directory = Net3_Proxy.Directory;
#endif
namespace IPA.Injector
{
/// <summary>
/// The entry point type for BSIPA's Doorstop injector.
/// </summary>
// ReSharper disable once UnusedMember.Global
internal static class Injector
{
private static Task? pluginAsyncLoadTask;
private static Task? permissionFixTask;
//private static string otherNewtonsoftJson = null;
// ReSharper disable once UnusedParameter.Global
internal static void Main(string[] args)
{ // entry point for doorstop
// At this point, literally nothing but mscorlib is loaded,
// and since this class doesn't have any static fields that
// aren't defined in mscorlib, we can control exactly what
// gets loaded.
_ = args;
try
{
var arguments = Environment.GetCommandLineArgs();
MaybeInitializeConsole(arguments);
SetupLibraryLoading();
EnsureDirectories();
// this is weird, but it prevents Mono from having issues loading the type.
// IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS
var unused = StandardLogger.PrintFilter;
#region // Above hack explanation
/*
* Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger
* is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to
* the above static property (or maybe any, but I don't really know) it behaves as expected and works fine.
*/
#endregion
Default.Debug("Initializing logger");
SelfConfig.ReadCommandLine(arguments);
SelfConfig.Load();
DisabledConfig.Load();
if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
Default.Error("Invalid installation; please buy the game to run BSIPA.");
return;
}
CriticalSection.Configure();
Logging.Logger.Injector.Debug("Prepping bootstrapper");
// make sure to load the game version and check boundaries before installing the bootstrap, because that uses the game assemblies property
GameVersionEarly.Load();
SelfConfig.Instance.CheckVersionBoundary();
// updates backup
InstallBootstrapPatch();
AntiMalwareEngine.Initialize();
Updates.InstallPendingUpdates();
Loader.LibLoader.SetupAssemblyFilenames(true);
pluginAsyncLoadTask = PluginLoader.LoadTask();
permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private static void MaybeInitializeConsole(string[] arguments)
{
var i = 0;
while (i < arguments.Length)
{
if (arguments[i++] == "--verbose")
{
if (i == arguments.Length)
{
WinConsole.Initialize(WinConsole.AttachParent);
return;
}
WinConsole.Initialize(int.TryParse(arguments[i], out int processId) ? processId : WinConsole.AttachParent);
return;
}
}
}
private static void EnsureDirectories()
{
string path;
if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData")))
_ = Directory.CreateDirectory(path);
if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins")))
_ = Directory.CreateDirectory(path);
}
private static void SetupLibraryLoading()
{
if (loadingDone) return;
loadingDone = true;
Loader.LibLoader.Configure();
}
private static void InstallBootstrapPatch()
{
var sw = Stopwatch.StartNew();
var cAsmName = Assembly.GetExecutingAssembly().GetName();
var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dataDir = new DirectoryInfo(managedPath).Parent!.Name;
var gameName = dataDir.Substring(0, dataDir.Length - 5);
Logging.Logger.Injector.Debug("Finding backup");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName);
var bkp = BackupManager.FindLatestBackup(backupPath);
if (bkp == null)
Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?");
// TODO: Investigate if this ever worked properly.
// this is a critical section because if you exit in here, assembly can die
using var critSec = CriticalSection.ExecuteSection();
var readerParameters = new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
};
Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists");
#region Insert patch into UnityEngine.CoreModule.dll
var unityPath = Path.Combine(managedPath, "UnityEngine.CoreModule.dll");
using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, readerParameters);
var unityModDef = unityAsmDef.MainModule;
bool modified = false;
foreach (var asmref in unityModDef.AssemblyReferences)
{
if (asmref.Name == cAsmName.Name)
{
if (asmref.Version != cAsmName.Version)
{
asmref.Version = cAsmName.Version;
modified = true;
}
}
}
var application = unityModDef.GetType("UnityEngine", "Camera");
if (application == null)
{
Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!"
+ "Nothing to patch to get ourselves into the Unity run cycle!");
goto endPatchCoreModule;
}
MethodDefinition? cctor = null;
foreach (var m in application.Methods)
if (m.IsRuntimeSpecialName && m.Name == ".cctor")
cctor = m;
var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method);
if (cctor == null)
{
cctor = new MethodDefinition(".cctor",
MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName,
unityModDef.TypeSystem.Void);
application.Methods.Add(cctor);
modified = true;
var ilp = cctor.Body.GetILProcessor();
ilp.Emit(OpCodes.Call, cbs);
ilp.Emit(OpCodes.Ret);
}
else
{
var ilp = cctor.Body.GetILProcessor();
for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++)
{
var ins = cctor.Body.Instructions[i];
switch (i)
{
case 0 when ins.OpCode != OpCodes.Call:
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
break;
case 0:
{
var methodRef = ins.Operand as MethodReference;
if (methodRef?.FullName != cbs.FullName)
{
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
}
break;
}
case 1 when ins.OpCode != OpCodes.Ret:
ilp.Replace(ins, ilp.Create(OpCodes.Ret));
modified = true;
break;
}
}
}
if (modified)
{
string tempFilePath = Path.GetTempFileName();
bkp?.Add(unityPath);
unityAsmDef.Write(tempFilePath);
File.Delete(unityPath);
File.Move(tempFilePath, unityPath);
}
endPatchCoreModule:
#endregion Insert patch into UnityEngine.CoreModule.dll
#if BeatSaber
Logging.Logger.Injector.Debug("Ensuring anti-yeet patch exists");
var name = SelfConfig.GameAssemblies_.FirstOrDefault() ?? SelfConfig.GetDefaultGameAssemblies().First();
var ascPath = Path.Combine(managedPath, name);
try
{
using var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, readerParameters);
var ascModDef = ascAsmDef.MainModule;
var deleter = ascModDef.GetType("IPAPluginsDirDeleter");
if (deleter.Methods.Count > 0)
{
deleter.Methods.Clear(); // delete all methods
string tempFilePath = Path.GetTempFileName();
bkp?.Add(ascPath);
ascAsmDef.Write(tempFilePath);
File.Delete(ascPath);
File.Move(tempFilePath, ascPath);
}
}
catch (Exception e)
{
Logging.Logger.Injector.Warn($"Could not apply anti-yeet patch to {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
Logging.Logger.Injector.Warn(e);
}
#endif
sw.Stop();
Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}");
}
private static bool bootstrapped;
private static void CreateBootstrapper()
{
if (bootstrapped) return;
bootstrapped = true;
Application.logMessageReceivedThreaded += delegate (string condition, string stackTrace, LogType type)
{
var level = UnityLogRedirector.LogTypeToLevel(type);
UnityLogProvider.UnityLogger.Log(level, $"{condition}");
UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}");
};
StdoutInterceptor.EnsureHarmonyLogging();
// need to reinit streams singe Unity seems to redirect stdout
StdoutInterceptor.RedirectConsole();
var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}
private static bool loadingDone;
private static void Bootstrapper_Destroyed()
{
// wait for plugins to finish loading
pluginAsyncLoadTask?.Wait();
permissionFixTask?.Wait();
Default.Debug("Plugins loaded");
Default.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP()));
_ = PluginComponent.Create();
}
}
}
#nullable enable
using IPA.AntiMalware;
using IPA.Config;
using IPA.Injector.Backups;
using IPA.Loader;
using IPA.Logging;
using IPA.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;
using static IPA.Logging.Logger;
using MethodAttributes = Mono.Cecil.MethodAttributes;
#if NET3
using Net3_Proxy;
using Path = Net3_Proxy.Path;
using File = Net3_Proxy.File;
using Directory = Net3_Proxy.Directory;
#endif
namespace IPA.Injector
{
/// <summary>
/// The entry point type for BSIPA's Doorstop injector.
/// </summary>
// ReSharper disable once UnusedMember.Global
internal static class Injector
{
private static Task? pluginAsyncLoadTask;
private static Task? permissionFixTask;
//private static string otherNewtonsoftJson = null;
// ReSharper disable once UnusedParameter.Global
internal static void Main(string[] args)
{ // entry point for doorstop
// At this point, literally nothing but mscorlib is loaded,
// and since this class doesn't have any static fields that
// aren't defined in mscorlib, we can control exactly what
// gets loaded.
_ = args;
try
{
var arguments = Environment.GetCommandLineArgs();
MaybeInitializeConsole(arguments);
SetupLibraryLoading();
EnsureDirectories();
// this is weird, but it prevents Mono from having issues loading the type.
// IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS
var unused = StandardLogger.PrintFilter;
#region // Above hack explanation
/*
* Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger
* is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to
* the above static property (or maybe any, but I don't really know) it behaves as expected and works fine.
*/
#endregion
Default.Debug("Initializing logger");
SelfConfig.ReadCommandLine(arguments);
SelfConfig.Load();
DisabledConfig.Load();
if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
Default.Error("Invalid installation; please buy the game to run BSIPA.");
return;
}
CriticalSection.Configure();
Logging.Logger.Injector.Debug("Prepping bootstrapper");
// make sure to load the game version and check boundaries before installing the bootstrap, because that uses the game assemblies property
GameVersionEarly.Load();
SelfConfig.Instance.CheckVersionBoundary();
// updates backup
InstallBootstrapPatch();
AntiMalwareEngine.Initialize();
Updates.InstallPendingUpdates();
Loader.LibLoader.SetupAssemblyFilenames(true);
pluginAsyncLoadTask = PluginLoader.LoadTask();
permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private static void MaybeInitializeConsole(string[] arguments)
{
var i = 0;
while (i < arguments.Length)
{
if (arguments[i++] == "--verbose")
{
if (i == arguments.Length)
{
WinConsole.Initialize(WinConsole.AttachParent);
return;
}
WinConsole.Initialize(int.TryParse(arguments[i], out int processId) ? processId : WinConsole.AttachParent);
return;
}
}
}
private static void EnsureDirectories()
{
string path;
if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData")))
_ = Directory.CreateDirectory(path);
if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins")))
_ = Directory.CreateDirectory(path);
}
private static void SetupLibraryLoading()
{
if (loadingDone) return;
loadingDone = true;
Loader.LibLoader.Configure();
}
private static void InstallBootstrapPatch()
{
var sw = Stopwatch.StartNew();
var cAsmName = Assembly.GetExecutingAssembly().GetName();
var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dataDir = new DirectoryInfo(managedPath).Parent!.Name;
var gameName = dataDir.Substring(0, dataDir.Length - 5);
Logging.Logger.Injector.Debug("Finding backup");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName);
var bkp = BackupManager.FindLatestBackup(backupPath);
if (bkp == null)
Logging.Logger.Injector.Warn("No backup found! Was BSIPA installed using the installer?");
// TODO: Investigate if this ever worked properly.
// this is a critical section because if you exit in here, assembly can die
using var critSec = CriticalSection.ExecuteSection();
var readerParameters = new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
};
Logging.Logger.Injector.Debug("Ensuring patch on UnityEngine.CoreModule exists");
#region Insert patch into UnityEngine.CoreModule.dll
var unityPath = Path.Combine(managedPath, "UnityEngine.CoreModule.dll");
using var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, readerParameters);
var unityModDef = unityAsmDef.MainModule;
bool modified = false;
foreach (var asmref in unityModDef.AssemblyReferences)
{
if (asmref.Name == cAsmName.Name)
{
if (asmref.Version != cAsmName.Version)
{
asmref.Version = cAsmName.Version;
modified = true;
}
}
}
var application = unityModDef.GetType("UnityEngine", "Camera");
if (application == null)
{
Logging.Logger.Injector.Critical("UnityEngine.CoreModule doesn't have a definition for UnityEngine.Camera!"
+ "Nothing to patch to get ourselves into the Unity run cycle!");
goto endPatchCoreModule;
}
MethodDefinition? cctor = null;
foreach (var m in application.Methods)
if (m.IsRuntimeSpecialName && m.Name == ".cctor")
cctor = m;
var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method);
if (cctor == null)
{
cctor = new MethodDefinition(".cctor",
MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName,
unityModDef.TypeSystem.Void);
application.Methods.Add(cctor);
modified = true;
var ilp = cctor.Body.GetILProcessor();
ilp.Emit(OpCodes.Call, cbs);
ilp.Emit(OpCodes.Ret);
}
else
{
var ilp = cctor.Body.GetILProcessor();
for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++)
{
var ins = cctor.Body.Instructions[i];
switch (i)
{
case 0 when ins.OpCode != OpCodes.Call:
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
break;
case 0:
{
var methodRef = ins.Operand as MethodReference;
if (methodRef?.FullName != cbs.FullName)
{
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
}
break;
}
case 1 when ins.OpCode != OpCodes.Ret:
ilp.Replace(ins, ilp.Create(OpCodes.Ret));
modified = true;
break;
}
}
}
if (modified)
{
string tempFilePath = Path.GetTempFileName();
bkp?.Add(unityPath);
unityAsmDef.Write(tempFilePath);
File.Delete(unityPath);
File.Move(tempFilePath, unityPath);
}
endPatchCoreModule:
#endregion Insert patch into UnityEngine.CoreModule.dll
sw.Stop();
Logging.Logger.Injector.Info($"Installing bootstrapper took {sw.Elapsed}");
}
private static bool bootstrapped;
private static void CreateBootstrapper()
{
if (bootstrapped) return;
bootstrapped = true;
Application.logMessageReceivedThreaded += delegate (string condition, string stackTrace, LogType type)
{
var level = UnityLogRedirector.LogTypeToLevel(type);
UnityLogProvider.UnityLogger.Log(level, $"{condition}");
UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}");
};
StdoutInterceptor.EnsureHarmonyLogging();
// need to reinit streams singe Unity seems to redirect stdout
StdoutInterceptor.RedirectConsole();
AntiYeetPatch.Apply();
var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}
private static bool loadingDone;
private static void Bootstrapper_Destroyed()
{
// wait for plugins to finish loading
pluginAsyncLoadTask?.Wait();
permissionFixTask?.Wait();
Default.Debug("Plugins loaded");
Default.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP()));
_ = PluginComponent.Create();
}
}
}

+ 1
- 1
IPA.Loader/Config/SelfConfig.cs View File

@ -87,7 +87,7 @@ namespace IPA.Config
} }
internal const string IPAName = "Beat Saber IPA"; internal const string IPAName = "Beat Saber IPA";
internal const string IPAVersion = "4.3.2.0";
internal const string IPAVersion = "4.3.3.0";
// uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug, // uses Updates.AutoUpdate, Updates.AutoCheckUpdates, YeetMods, Debug.ShowCallSource, Debug.ShowDebug,
// Debug.CondenseModLogs // Debug.CondenseModLogs


+ 11
- 4
IPA.Loader/Loader/Composite/CompositeBSPlugin.cs View File

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
using Logger = IPA.Logging.Logger; using Logger = IPA.Logging.Logger;
@ -10,9 +12,9 @@ namespace IPA.Loader.Composite
{ {
private readonly IEnumerable<PluginExecutor> plugins; private readonly IEnumerable<PluginExecutor> plugins;
private delegate void CompositeCall(PluginExecutor plugin);
public CompositeBSPlugin(IEnumerable<PluginExecutor> plugins)
private delegate Task CompositeCall(PluginExecutor plugin);
public CompositeBSPlugin(IEnumerable<PluginExecutor> plugins)
{ {
this.plugins = plugins; this.plugins = plugins;
} }
@ -23,7 +25,12 @@ namespace IPA.Loader.Composite
try try
{ {
if (plugin != null) if (plugin != null)
callback(plugin);
{
callback(plugin).ContinueWith(t =>
{
Logger.Default.Error($"{plugin.Metadata.Name} {method}: {t.Exception!.InnerException}");
}, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
}
} }
catch (Exception ex) catch (Exception ex)
{ {


+ 36
- 14
IPA.Loader/Loader/PluginExecutor.cs View File

@ -38,7 +38,7 @@ namespace IPA.Loader
if (specialType != Special.None) if (specialType != Special.None)
{ {
CreatePlugin = m => null; CreatePlugin = m => null;
LifecycleEnable = o => { };
LifecycleEnable = o => TaskEx.WhenAll();
LifecycleDisable = o => TaskEx.WhenAll(); LifecycleDisable = o => TaskEx.WhenAll();
} }
else else
@ -48,8 +48,7 @@ namespace IPA.Loader
public object Instance { get; private set; } = null; public object Instance { get; private set; } = null;
private Func<PluginMetadata, object> CreatePlugin { get; set; } private Func<PluginMetadata, object> CreatePlugin { get; set; }
private Action<object> LifecycleEnable { get; set; }
// disable may be async (#24)
private Func<object, Task> LifecycleEnable { get; set; }
private Func<object, Task> LifecycleDisable { get; set; } private Func<object, Task> LifecycleDisable { get; set; }
public void Create() public void Create()
@ -58,7 +57,7 @@ namespace IPA.Loader
Instance = CreatePlugin(Metadata); Instance = CreatePlugin(Metadata);
} }
public void Enable() => LifecycleEnable(Instance);
public Task Enable() => LifecycleEnable(Instance);
public Task Disable() => LifecycleDisable(Instance); public Task Disable() => LifecycleDisable(Instance);
@ -120,7 +119,7 @@ namespace IPA.Loader
return createExpr.Compile(); return createExpr.Compile();
} }
// TODO: make enable and disable able to take a bool indicating which it is // TODO: make enable and disable able to take a bool indicating which it is
private static Action<object> MakeLifecycleEnableFunc(Type type, string name)
private static Func<object, Task> MakeLifecycleEnableFunc(Type type, string name)
{ {
var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() is not null; var noEnableDisable = type.GetCustomAttribute<NoEnableDisableAttribute>() is not null;
var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
@ -132,24 +131,47 @@ namespace IPA.Loader
{ {
if (!noEnableDisable) if (!noEnableDisable)
Logger.Loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); Logger.Loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?");
return o => { };
return o => TaskEx.WhenAll();
} }
var taskMethods = new List<MethodInfo>();
var nonTaskMethods = new List<MethodInfo>();
foreach (var m in enableMethods) foreach (var m in enableMethods)
{ {
if (m.GetParameters().Length > 0) if (m.GetParameters().Length > 0)
throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters."); throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters.");
if (m.ReturnType != typeof(void)) if (m.ReturnType != typeof(void))
Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored.");
{
if (typeof(Task).IsAssignableFrom(m.ReturnType))
{
taskMethods.Add(m);
continue;
}
Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a non-Task value. It will be ignored.");
}
nonTaskMethods.Add(m);
} }
Expression<Func<Task>> completedTaskDel = () => TaskEx.WhenAll();
var getCompletedTask = completedTaskDel.Body;
var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) });
var objParam = Expression.Parameter(typeof(object), "obj"); var objParam = Expression.Parameter(typeof(object), "obj");
var instVar = ExpressionEx.Variable(type, "inst"); var instVar = ExpressionEx.Variable(type, "inst");
var createExpr = Expression.Lambda<Action<object>>(
var createExpr = Expression.Lambda<Func<object, Task>>(
ExpressionEx.Block(new[] { instVar }, ExpressionEx.Block(new[] { instVar },
enableMethods
nonTaskMethods
.Select(m => (Expression)Expression.Call(instVar, m)) .Select(m => (Expression)Expression.Call(instVar, m))
.Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))),
.Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))
.Append(
taskMethods.Count == 0
? getCompletedTask
: Expression.Call(taskWhenAll,
Expression.NewArrayInit(typeof(Task),
taskMethods.Select(m =>
(Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
objParam); objParam);
return createExpr.Compile(); return createExpr.Compile();
} }
@ -181,8 +203,8 @@ namespace IPA.Loader
taskMethods.Add(m); taskMethods.Add(m);
continue; continue;
} }
else
Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored.");
Logger.Loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored.");
} }
nonTaskMethods.Add(m); nonTaskMethods.Add(m);
@ -200,11 +222,11 @@ namespace IPA.Loader
.Select(m => (Expression)Expression.Call(instVar, m)) .Select(m => (Expression)Expression.Call(instVar, m))
.Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type))) .Prepend(ExpressionEx.Assign(instVar, Expression.Convert(objParam, type)))
.Append( .Append(
taskMethods.Count == 0
taskMethods.Count == 0
? getCompletedTask ? getCompletedTask
: Expression.Call(taskWhenAll, : Expression.Call(taskWhenAll,
Expression.NewArrayInit(typeof(Task), Expression.NewArrayInit(typeof(Task),
taskMethods.Select(m =>
taskMethods.Select(m =>
(Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))), (Expression)Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))),
objParam); objParam);
return createExpr.Compile(); return createExpr.Compile();


+ 1
- 1
IPA.Loader/Loader/manifest.json View File

@ -7,7 +7,7 @@
], ],
"id": "BSIPA", "id": "BSIPA",
"name": "Beat Saber IPA", "name": "Beat Saber IPA",
"version": "4.3.2",
"version": "4.3.3",
"icon": "IPA.icon_white.png", "icon": "IPA.icon_white.png",
"features": { "features": {
"IPA.DefineFeature": [ "IPA.DefineFeature": [


+ 1
- 1
IPA/Program.cs View File

@ -21,7 +21,7 @@ namespace IPA
Unknown Unknown
} }
public const string FileVersion = "4.3.2.0";
public const string FileVersion = "4.3.3.0";
public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!; public static Version Version => Assembly.GetEntryAssembly()!.GetName().Version!;


Loading…
Cancel
Save