Browse Source

Switched entirely over to attribute based system

pull/46/head
Anairkoen Schno 4 years ago
parent
commit
5eda901607
9 changed files with 1352 additions and 1243 deletions
  1. +341
    -341
      IPA.Injector/Injector.cs
  2. +68
    -67
      IPA.Loader/Loader/Composite/CompositeBSPlugin.cs
  3. +3
    -3
      IPA.Loader/Loader/Features/Feature.cs
  4. +30
    -20
      IPA.Loader/Loader/PluginExecutor.cs
  5. +11
    -6
      IPA.Loader/Loader/PluginInitInjector.cs
  6. +114
    -13
      IPA.Loader/Loader/PluginLoader.cs
  7. +15
    -16
      IPA.Loader/Loader/PluginManager.cs
  8. +1
    -0
      IPA.Loader/Logging/StandardLogger.cs
  9. +769
    -777
      IPA.Loader/Updating/BeatMods/Updater.cs

+ 341
- 341
IPA.Injector/Injector.cs View File

@ -1,342 +1,342 @@
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.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.
try
{
if (Environment.GetCommandLineArgs().Contains("--verbose"))
WinConsole.Initialize();
SetupLibraryLoading();
/*var otherNewtonsoft = Path.Combine(
Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(),
"Managed",
"Newtonsoft.Json.dll");
if (File.Exists(otherNewtonsoft))
{ // this game ships its own Newtonsoft; force load ours and flag loading theirs
LibLoader.LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"));
otherNewtonsoftJson = otherNewtonsoft;
}*/
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 explaination
/*
* 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
log.Debug("Initializing logger");
SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs());
SelfConfig.Load();
DisabledConfig.Load();
if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
loader.Error("Invalid installation; please buy the game to run BSIPA.");
return;
}
CriticalSection.Configure();
loader.Debug("Prepping bootstrapper");
// updates backup
InstallBootstrapPatch();
GameVersionEarly.Load();
Updates.InstallPendingUpdates();
LibLoader.SetupAssemblyFilenames(true);
pluginAsyncLoadTask = PluginLoader.LoadTask();
permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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;
LibLoader.Configure();
}
private static void InstallHarmonyProtections()
{ // proxy function to delay resolution
HarmonyProtectorProxy.ProtectNull();
}
private static void InstallBootstrapPatch()
{
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);
loader.Debug("Finding backup");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName);
var bkp = BackupManager.FindLatestBackup(backupPath);
if (bkp == null)
loader.Warn("No backup found! Was BSIPA installed using the installer?");
loader.Debug("Ensuring patch on UnityEngine.CoreModule exists");
#region Insert patch into UnityEngine.CoreModule.dll
{
var unityPath = Path.Combine(managedPath,
"UnityEngine.CoreModule.dll");
// this is a critical section because if you exit in here, CoreModule can die
CriticalSection.EnterExecuteSection();
var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
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", "Application");
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)
{
bkp?.Add(unityPath);
unityAsmDef.Write(unityPath);
}
CriticalSection.ExitExecuteSection();
}
#endregion Insert patch into UnityEngine.CoreModule.dll
loader.Debug("Ensuring game assemblies are virtualized");
#region Virtualize game assemblies
bool isFirst = true;
foreach(var name in SelfConfig.GameAssemblies_)
{
var ascPath = Path.Combine(managedPath, name);
CriticalSection.EnterExecuteSection();
try
{
loader.Debug($"Virtualizing {name}");
using var ascModule = VirtualizedModule.Load(ascPath);
ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath));
}
catch (Exception e)
{
loader.Error($"Could not virtualize {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
loader.Error(e);
}
if (isFirst)
{
try
{
loader.Debug("Applying anti-yeet patch");
var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
var ascModDef = ascAsmDef.MainModule;
var deleter = ascModDef.GetType("IPAPluginsDirDeleter");
deleter.Methods.Clear(); // delete all methods
ascAsmDef.Write(ascPath);
isFirst = false;
}
catch (Exception e)
{
loader.Warn($"Could not apply anti-yeet patch to {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
loader.Warn(e);
}
}
CriticalSection.ExitExecuteSection();
}
#endregion
}
private static bool bootstrapped;
private static void CreateBootstrapper()
{
if (bootstrapped) return;
bootstrapped = true;
/*if (otherNewtonsoftJson != null)
Assembly.LoadFrom(otherNewtonsoftJson);*/
#if DEBUG
Config.Stores.GeneratedStore.DebugSaveAssembly("GeneratedAssembly.dll");
#endif
Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type)
{
var level = UnityLogRedirector.LogTypeToLevel(type);
UnityLogProvider.UnityLogger.Log(level, $"{condition}");
UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}");
};
// need to reinit streams singe Unity seems to redirect stdout
StdoutInterceptor.RedirectConsole();
InstallHarmonyProtections();
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();
BeatSaber.EnsureRuntimeGameVersion();
log.Debug("Plugins loaded");
log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP()));
PluginComponent.Create();
}
}
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.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.
try
{
if (Environment.GetCommandLineArgs().Contains("--verbose"))
WinConsole.Initialize();
SetupLibraryLoading();
/*var otherNewtonsoft = Path.Combine(
Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(),
"Managed",
"Newtonsoft.Json.dll");
if (File.Exists(otherNewtonsoft))
{ // this game ships its own Newtonsoft; force load ours and flag loading theirs
LibLoader.LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed"));
otherNewtonsoftJson = otherNewtonsoft;
}*/
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 explaination
/*
* 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
log.Debug("Initializing logger");
SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs());
SelfConfig.Load();
DisabledConfig.Load();
if (AntiPiracy.IsInvalid(Environment.CurrentDirectory))
{
loader.Error("Invalid installation; please buy the game to run BSIPA.");
return;
}
CriticalSection.Configure();
loader.Debug("Prepping bootstrapper");
// updates backup
InstallBootstrapPatch();
GameVersionEarly.Load();
Updates.InstallPendingUpdates();
LibLoader.SetupAssemblyFilenames(true);
pluginAsyncLoadTask = PluginLoader.LoadTask();
permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory));
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
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;
LibLoader.Configure();
}
private static void InstallHarmonyProtections()
{ // proxy function to delay resolution
HarmonyProtectorProxy.ProtectNull();
}
private static void InstallBootstrapPatch()
{
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);
loader.Debug("Finding backup");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName);
var bkp = BackupManager.FindLatestBackup(backupPath);
if (bkp == null)
loader.Warn("No backup found! Was BSIPA installed using the installer?");
loader.Debug("Ensuring patch on UnityEngine.CoreModule exists");
#region Insert patch into UnityEngine.CoreModule.dll
{
var unityPath = Path.Combine(managedPath,
"UnityEngine.CoreModule.dll");
// this is a critical section because if you exit in here, CoreModule can die
CriticalSection.EnterExecuteSection();
var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
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", "Application");
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)
{
bkp?.Add(unityPath);
unityAsmDef.Write(unityPath);
}
CriticalSection.ExitExecuteSection();
}
#endregion Insert patch into UnityEngine.CoreModule.dll
loader.Debug("Ensuring game assemblies are virtualized");
#region Virtualize game assemblies
bool isFirst = true;
foreach(var name in SelfConfig.GameAssemblies_)
{
var ascPath = Path.Combine(managedPath, name);
CriticalSection.EnterExecuteSection();
try
{
loader.Debug($"Virtualizing {name}");
using var ascModule = VirtualizedModule.Load(ascPath);
ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath));
}
catch (Exception e)
{
loader.Error($"Could not virtualize {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
loader.Error(e);
}
if (isFirst)
{
try
{
loader.Debug("Applying anti-yeet patch");
var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
var ascModDef = ascAsmDef.MainModule;
var deleter = ascModDef.GetType("IPAPluginsDirDeleter");
deleter.Methods.Clear(); // delete all methods
ascAsmDef.Write(ascPath);
isFirst = false;
}
catch (Exception e)
{
loader.Warn($"Could not apply anti-yeet patch to {ascPath}");
if (SelfConfig.Debug_.ShowHandledErrorStackTraces_)
loader.Warn(e);
}
}
CriticalSection.ExitExecuteSection();
}
#endregion
}
private static bool bootstrapped;
private static void CreateBootstrapper()
{
if (bootstrapped) return;
bootstrapped = true;
/*if (otherNewtonsoftJson != null)
Assembly.LoadFrom(otherNewtonsoftJson);*/
Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type)
{
var level = UnityLogRedirector.LogTypeToLevel(type);
UnityLogProvider.UnityLogger.Log(level, $"{condition}");
UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}");
};
// need to reinit streams singe Unity seems to redirect stdout
StdoutInterceptor.RedirectConsole();
InstallHarmonyProtections();
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();
BeatSaber.EnsureRuntimeGameVersion();
log.Debug("Plugins loaded");
log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP()));
PluginComponent.Create();
#if DEBUG
Config.Stores.GeneratedStore.DebugSaveAssembly("GeneratedAssembly.dll");
#endif
}
}
}

+ 68
- 67
IPA.Loader/Loader/Composite/CompositeBSPlugin.cs View File

@ -1,68 +1,69 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine.SceneManagement;
using Logger = IPA.Logging.Logger;
namespace IPA.Loader.Composite
{
internal class CompositeBSPlugin
{
private readonly IEnumerable<PluginLoader.PluginInfo> plugins;
private delegate void CompositeCall(PluginLoader.PluginInfo plugin);
public CompositeBSPlugin(IEnumerable<PluginLoader.PluginInfo> plugins)
{
this.plugins = plugins;
}
private void Invoke(CompositeCall callback, [CallerMemberName] string method = "")
{
foreach (var plugin in plugins)
{
try
{
if (plugin.Plugin != null)
callback(plugin);
}
catch (Exception ex)
{
Logger.log.Error($"{plugin.Metadata.Name} {method}: {ex}");
}
}
}
public void OnEnable()
=> Invoke(plugin => plugin.Plugin.OnEnable());
public void OnApplicationQuit()
=> Invoke(plugin => plugin.Plugin.OnApplicationQuit());
public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
=> Invoke(plugin => plugin.Plugin.OnSceneLoaded(scene, sceneMode));
public void OnSceneUnloaded(Scene scene)
=> Invoke(plugin => plugin.Plugin.OnSceneUnloaded(scene));
public void OnActiveSceneChanged(Scene prevScene, Scene nextScene)
=> Invoke(plugin => plugin.Plugin.OnActiveSceneChanged(prevScene, nextScene));
public void OnUpdate()
=> Invoke(plugin => {
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnUpdate();
});
public void OnFixedUpdate()
=> Invoke(plugin => {
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnFixedUpdate();
});
public void OnLateUpdate()
=> Invoke(plugin => {
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnLateUpdate();
});
}
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine.SceneManagement;
using Logger = IPA.Logging.Logger;
namespace IPA.Loader.Composite
{
internal class CompositeBSPlugin
{
private readonly IEnumerable<PluginExecutor> plugins;
private delegate void CompositeCall(PluginExecutor plugin);
public CompositeBSPlugin(IEnumerable<PluginExecutor> plugins)
{
this.plugins = plugins;
}
private void Invoke(CompositeCall callback, [CallerMemberName] string method = "")
{
foreach (var plugin in plugins)
{
try
{
if (plugin != null)
callback(plugin);
}
catch (Exception ex)
{
Logger.log.Error($"{plugin.Metadata.Name} {method}: {ex}");
}
}
}
public void OnEnable()
=> Invoke(plugin => plugin.Enable());
public void OnApplicationQuit() // do something useful with the Task that Disable gives us
=> Invoke(plugin => plugin.Disable());
public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
{ }//=> Invoke(plugin => plugin.Plugin.OnSceneLoaded(scene, sceneMode));
public void OnSceneUnloaded(Scene scene)
{ }//=> Invoke(plugin => plugin.Plugin.OnSceneUnloaded(scene));
public void OnActiveSceneChanged(Scene prevScene, Scene nextScene)
{ }//=> Invoke(plugin => plugin.Plugin.OnActiveSceneChanged(prevScene, nextScene));
public void OnUpdate()
{ }/*=> Invoke(plugin =>
{
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnUpdate();
});*/
public void OnFixedUpdate()
{ }/*=> Invoke(plugin => {
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnFixedUpdate();
});*/
public void OnLateUpdate()
{ }/*=> Invoke(plugin => {
if (plugin.Plugin is IEnhancedPlugin saberPlugin)
saberPlugin.OnLateUpdate();
});*/
}
}

+ 3
- 3
IPA.Loader/Loader/Features/Feature.cs View File

@ -61,20 +61,20 @@ namespace IPA.Loader.Features
/// </summary>
/// <param name="plugin">the plugin to be initialized</param>
/// <returns>whether or not to call the Init method</returns>
public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true;
public virtual bool BeforeInit(PluginMetadata plugin) => true;
/// <summary>
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception.
/// </summary>
/// <param name="plugin">the plugin that was just initialized</param>
/// <param name="pluginInstance">the instance of the plugin being initialized</param>
public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin);
public virtual void AfterInit(PluginMetadata plugin, object pluginInstance) => AfterInit(plugin);
/// <summary>
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception.
/// </summary>
/// <param name="plugin">the plugin that was just initialized</param>
public virtual void AfterInit(PluginLoader.PluginInfo plugin) { }
public virtual void AfterInit(PluginMetadata plugin) { }
/// <summary>
/// Ensures a plugin's assembly is loaded. Do not use unless you need to.


+ 30
- 20
IPA.Loader/Loader/PluginExecutor.cs View File

@ -7,6 +7,7 @@ using System.Reflection;
using System.Linq.Expressions;
#if NET4
using Task = System.Threading.Tasks.Task;
using TaskEx = System.Threading.Tasks.Task;
#endif
#if NET3
using Net3_Proxy;
@ -20,14 +21,21 @@ namespace IPA.Loader
internal class PluginExecutor
{
public PluginMetadata Metadata { get; }
public PluginExecutor(PluginMetadata meta)
public PluginExecutor(PluginMetadata meta, bool isSelf)
{
Metadata = meta;
PrepareDelegates();
if (isSelf)
{
CreatePlugin = m => null;
LifecycleEnable = o => { };
LifecycleDisable = o => TaskEx.CompletedTask;
}
else
PrepareDelegates();
}
private object pluginObject = null;
public object Instance { get; private set; } = null;
private Func<PluginMetadata, object> CreatePlugin { get; set; }
private Action<object> LifecycleEnable { get; set; }
// disable may be async (#24)
@ -35,12 +43,12 @@ namespace IPA.Loader
public void Create()
{
if (pluginObject != null) return;
pluginObject = CreatePlugin(Metadata);
if (Instance != null) return;
Instance = CreatePlugin(Metadata);
}
public void Enable() => LifecycleEnable(pluginObject);
public Task Disable() => LifecycleDisable(pluginObject);
public void Enable() => LifecycleEnable(Instance);
public Task Disable() => LifecycleDisable(Instance);
private void PrepareDelegates()
@ -85,21 +93,23 @@ namespace IPA.Loader
}
// TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3?
var metaParam = Expression.Parameter(typeof(PluginMetadata));
var objVar = Expression.Variable(type);
var metaParam = Expression.Parameter(typeof(PluginMetadata), "meta");
var objVar = Expression.Variable(type, "objVar");
var persistVar = Expression.Variable(typeof(object), "persistVar");
var createExpr = Expression.Lambda<Func<PluginMetadata, object>>(
Expression.Block(
Expression.Block(new[] { objVar, persistVar },
initMethods
.Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es)))
.Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, persistVar, es => Expression.Call(objVar, m, es)))
.Prepend(Expression.Assign(objVar,
usingDefaultCtor
? Expression.New(ctor)
: PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es))))
: PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, persistVar, es => Expression.New(ctor, es))))
.Append(Expression.Convert(objVar, typeof(object)))),
metaParam);
// TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler
return createExpr.Compile();
}
// TODO: make enable and disable able to take a bool indicating which it is
private static Action<object> MakeLifecycleEnableFunc(Type type, string name)
{
var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
@ -121,10 +131,10 @@ namespace IPA.Loader
Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored.");
}
var objParam = Expression.Parameter(typeof(object));
var instVar = Expression.Variable(type);
var objParam = Expression.Parameter(typeof(object), "obj");
var instVar = Expression.Variable(type, "inst");
var createExpr = Expression.Lambda<Action<object>>(
Expression.Block(
Expression.Block(new[] { instVar },
enableMethods
.Select(m => Expression.Call(instVar, m))
.Prepend<Expression>(Expression.Assign(instVar, Expression.Convert(objParam, type)))),
@ -164,14 +174,14 @@ namespace IPA.Loader
nonTaskMethods.Add(m);
}
Expression<Func<Task>> completedTaskDel = () => Task.CompletedTask;
Expression<Func<Task>> completedTaskDel = () => TaskEx.CompletedTask;
var getCompletedTask = completedTaskDel.Body;
var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static);
var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) });
var objParam = Expression.Parameter(typeof(object));
var instVar = Expression.Variable(type);
var objParam = Expression.Parameter(typeof(object), "obj");
var instVar = Expression.Variable(type, "inst");
var createExpr = Expression.Lambda<Func<object, Task>>(
Expression.Block(
Expression.Block(new[] { instVar },
nonTaskMethods
.Select(m => Expression.Call(instVar, m))
.Prepend<Expression>(Expression.Assign(instVar, Expression.Convert(objParam, type)))


+ 11
- 6
IPA.Loader/Loader/PluginInitInjector.cs View File

@ -96,22 +96,27 @@ namespace IPA.Loader
}
private static readonly MethodInfo InjectMethod = typeof(PluginInitInjector).GetMethod(nameof(Inject), BindingFlags.NonPublic | BindingFlags.Static);
internal static Expression InjectedCallExpr(ParameterInfo[] initParams, Expression meta, Func<IEnumerable<Expression>, Expression> exprGen)
internal static Expression InjectedCallExpr(ParameterInfo[] initParams, Expression meta, ParameterExpression persistVar, Func<IEnumerable<Expression>, Expression> exprGen)
{
var arr = Expression.Variable(typeof(object[]));
return Expression.Block(
Expression.Assign(arr, Expression.Call(InjectMethod, Expression.Constant(initParams), meta)),
var arr = Expression.Variable(typeof(object[]), "initArr");
return Expression.Block(new[] { arr },
Expression.Assign(arr, Expression.Call(InjectMethod, Expression.Constant(initParams), meta, persistVar)),
exprGen(initParams
.Select(p => p.ParameterType)
.Select((t, i) => Expression.Convert(
Expression.ArrayIndex(arr, Expression.Constant(i)), t))));
}
internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta)
internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta, ref object persist)
{
var initArgs = new List<object>();
var previousValues = new Dictionary<TypedInjector, object>(injectors.Count);
var previousValues = persist as Dictionary<TypedInjector, object>;
if (previousValues == null)
{
previousValues = new Dictionary<TypedInjector, object>(injectors.Count);
persist = previousValues;
}
foreach (var param in initParams)
{


+ 114
- 13
IPA.Loader/Loader/PluginLoader.cs View File

@ -622,15 +622,118 @@ namespace IPA.Loader
meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
}
internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
internal static PluginExecutor InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
{
if (meta.IsAttributePlugin)
if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
if (!meta.IsAttributePlugin)
{
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Non-attribute plugins are currently not supported" });
return null;
}
if (meta.IsSelf)
return new PluginExecutor(meta, true);
foreach (var dep in meta.Dependencies)
{
if (alreadyLoaded.Contains(dep)) continue;
// otherwise...
if (ignoredPlugins.TryGetValue(dep, out var reason))
{ // was added to the ignore list
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
{
ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
RelatedTo = dep
});
}
else
{ // was not added to ignore list
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
{
ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
RelatedTo = dep
});
}
return null;
}
Load(meta);
foreach (var feature in meta.Features)
{
if (!feature.BeforeLoad(meta))
{
Logger.loader.Warn(
$"Feature {feature?.GetType()} denied plugin {meta.Name} from loading! {feature?.InvalidMessage}");
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
{
ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}"
});
return null;
}
}
PluginExecutor exec;
try
{
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Attribute plugins are currently not supported" });
exec = new PluginExecutor(meta, false);
}
catch (Exception e)
{
Logger.loader.Error($"Error creating executor for {meta.Name}");
Logger.loader.Error(e);
return null;
}
foreach (var feature in meta.Features)
{
if (!feature.BeforeInit(meta))
{
Logger.loader.Warn(
$"Feature {feature?.GetType()} denied plugin {meta.Name} from initializing! {feature?.InvalidMessage}");
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
{
ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}"
});
return null;
}
}
try
{
exec.Create();
}
catch (Exception e)
{
Logger.loader.Error($"Could not init plugin {meta.Name}");
Logger.loader.Error(e);
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
{
ReasonText = "Error ocurred while initializing",
Error = e
});
return null;
}
if (meta.PluginType == null)
foreach (var feature in meta.Features)
try
{
feature.AfterInit(meta, exec.Instance);
}
catch (Exception e)
{
Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
}
return exec;
#region Interface plugin support
/*if (meta.IsSelf)
return new PluginInfo()
{
Metadata = meta,
@ -639,9 +742,6 @@ namespace IPA.Loader
var info = new PluginInfo();
if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
try
{
foreach (var dep in meta.Dependencies)
@ -738,22 +838,23 @@ namespace IPA.Loader
return null;
}
return info;
return info;*/
#endregion
}
internal static List<PluginInfo> LoadPlugins()
internal static List<PluginExecutor> LoadPlugins()
{
InitFeatures();
DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
var list = new List<PluginInfo>();
var list = new List<PluginExecutor>();
var loaded = new HashSet<PluginMetadata>();
foreach (var meta in PluginsMetadata)
{
var info = InitPlugin(meta, loaded);
if (info != null)
var exec = InitPlugin(meta, loaded);
if (exec != null)
{
list.Add(info);
list.Add(exec);
loaded.Add(meta);
}
}


+ 15
- 16
IPA.Loader/Loader/PluginManager.cs View File

@ -33,19 +33,16 @@ namespace IPA.Loader
/// <summary>
/// An <see cref="IEnumerable"/> of new Beat Saber plugins
/// </summary>
internal static IEnumerable<IPlugin> BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin);
private static List<PluginInfo> _bsPlugins;
internal static IEnumerable<PluginInfo> BSMetas => _bsPlugins;
private static List<PluginExecutor> _bsPlugins;
internal static IEnumerable<PluginExecutor> BSMetas => _bsPlugins;
/// <summary>
/// Gets info about the plugin with the specified name.
/// </summary>
/// <param name="name">the name of the plugin to get (must be an exact match)</param>
/// <returns>the plugin info for the requested plugin or null</returns>
public static PluginInfo GetPlugin(string name)
{
return BSMetas.FirstOrDefault(p => p.Metadata.Name == name);
}
public static PluginMetadata GetPlugin(string name)
=> BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Name == name);
/// <summary>
/// Gets info about the plugin with the specified ModSaber name.
@ -53,17 +50,15 @@ namespace IPA.Loader
/// <param name="name">the ModSaber name of the plugin to get (must be an exact match)</param>
/// <returns>the plugin info for the requested plugin or null</returns>
[Obsolete("Old name. Use GetPluginFromId instead.")]
public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name);
public static PluginMetadata GetPluginFromModSaberName(string name) => GetPluginFromId(name);
/// <summary>
/// Gets info about the plugin with the specified ID.
/// </summary>
/// <param name="name">the ID name of the plugin to get (must be an exact match)</param>
/// <returns>the plugin info for the requested plugin or null</returns>
public static PluginInfo GetPluginFromId(string name)
{
return BSMetas.FirstOrDefault(p => p.Metadata.Id == name);
}
public static PluginMetadata GetPluginFromId(string name)
=> BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Id == name);
/// <summary>
/// Gets a disabled plugin's metadata by its name.
@ -81,6 +76,8 @@ namespace IPA.Loader
public static PluginMetadata GetDisabledPluginFromId(string name) =>
DisabledPlugins.FirstOrDefault(p => p.Id == name);
// TODO: rewrite below
/*
/// <summary>
/// Disables a plugin, and all dependents.
/// </summary>
@ -220,7 +217,7 @@ namespace IPA.Loader
/// <param name="pluginId">the ID, or name if the ID is null, of the plugin to enable</param>
/// <returns>whether a restart is needed to activate</returns>
public static bool EnablePlugin(string pluginId) =>
EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId));
EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId));*/
/// <summary>
/// Checks if a given plugin is disabled.
@ -269,8 +266,9 @@ namespace IPA.Loader
/// Gets a list of all BSIPA plugins.
/// </summary>
/// <value>a collection of all enabled plugins as <see cref="PluginInfo"/>s</value>
public static IEnumerable<PluginInfo> AllPlugins => BSMetas;
public static IEnumerable<PluginMetadata> AllPlugins => BSMetas.Select(p => p.Metadata);
/*
/// <summary>
/// Converts a plugin's metadata to a <see cref="PluginInfo"/>.
/// </summary>
@ -281,8 +279,9 @@ namespace IPA.Loader
if (IsDisabled(meta))
return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta);
else
return AllPlugins.FirstOrDefault(p => p.Metadata == meta);
return AllPlugins.FirstOrDefault(p => p == meta);
}
*/
/// <summary>
/// An <see cref="IEnumerable"/> of old IPA plugins.
@ -301,7 +300,7 @@ namespace IPA.Loader
// Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL,
// so we need to resort to P/Invoke
string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath);
_bsPlugins = new List<PluginInfo>();
_bsPlugins = new List<PluginExecutor>();
_ipaPlugins = new List<Old.IPlugin>();
if (!Directory.Exists(pluginDirectory)) return;


+ 1
- 0
IPA.Loader/Logging/StandardLogger.cs View File

@ -193,6 +193,7 @@ namespace IPA.Logging
if (message == null)
throw new ArgumentNullException(nameof(message));
// FIXME: trace doesn't seem to ever actually appear
if (!showTrace && level == Level.Trace) return;
// make sure that the queue isn't being cleared


+ 769
- 777
IPA.Loader/Updating/BeatMods/Updater.cs
File diff suppressed because it is too large
View File


Loading…
Cancel
Save