diff --git a/BSIPA.sln b/BSIPA.sln
index 548da4f3..f0444c68 100644
--- a/BSIPA.sln
+++ b/BSIPA.sln
@@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Tests", "IPA.Tests\IPA.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSBuildTasks", "MSBuildTasks\MSBuildTasks.csproj", "{F08C3C7A-3221-432E-BAB8-32BCE58408C8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Loader", "IPA.Loader\IPA.Loader.csproj", "{5AD344F0-01A0-4CA8-92E5-9D095737744D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Injector", "IPA.Injector\IPA.Injector.csproj", "{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -43,6 +47,14 @@ Global
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AD344F0-01A0-4CA8-92E5-9D095737744D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AD344F0-01A0-4CA8-92E5-9D095737744D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AD344F0-01A0-4CA8-92E5-9D095737744D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AD344F0-01A0-4CA8-92E5-9D095737744D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/IPA.Injector/Bootstrapper.cs b/IPA.Injector/Bootstrapper.cs
new file mode 100644
index 00000000..1f8e9a78
--- /dev/null
+++ b/IPA.Injector/Bootstrapper.cs
@@ -0,0 +1,31 @@
+using IllusionInjector.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+
+namespace IPA.Injector
+{
+ class Bootstrapper : MonoBehaviour
+ {
+ public event Action Destroyed = delegate {};
+
+ void Awake()
+ {
+ //if (Environment.CommandLine.Contains("--verbose"))
+ //{
+ Windows.GuiConsole.CreateConsole();
+ //}
+ }
+
+ void Start()
+ {
+ Destroy(gameObject);
+ }
+ void OnDestroy()
+ {
+ Destroyed();
+ }
+ }
+}
diff --git a/IPA.Injector/ConsoleWindow.cs b/IPA.Injector/ConsoleWindow.cs
new file mode 100644
index 00000000..acee7433
--- /dev/null
+++ b/IPA.Injector/ConsoleWindow.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections;
+using System.Runtime.InteropServices;
+using System.IO;
+using System.Text;
+using Microsoft.Win32.SafeHandles;
+
+namespace Windows
+{
+
+ class GuiConsole
+ {
+ public static void CreateConsole()
+ {
+ if (hasConsole)
+ return;
+ if (oldOut == IntPtr.Zero)
+ oldOut = GetStdHandle( -11 );
+ if (! AllocConsole())
+ throw new Exception("AllocConsole() failed");
+ conOut = CreateFile( "CONOUT$", 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero );
+ if (! SetStdHandle(-11, conOut))
+ throw new Exception("SetStdHandle() failed");
+ StreamToConsole();
+ hasConsole = true;
+ }
+ public static void ReleaseConsole()
+ {
+ if (! hasConsole)
+ return;
+ if (! CloseHandle(conOut))
+ throw new Exception("CloseHandle() failed");
+ conOut = IntPtr.Zero;
+ if (! FreeConsole())
+ throw new Exception("FreeConsole() failed");
+ if (! SetStdHandle(-11, oldOut))
+ throw new Exception("SetStdHandle() failed");
+ StreamToConsole();
+ hasConsole = false;
+ }
+ private static void StreamToConsole()
+ {
+ Stream cstm = Console.OpenStandardOutput();
+ StreamWriter cstw = new StreamWriter( cstm, Encoding.Default );
+ cstw.AutoFlush = true;
+ Console.SetOut( cstw );
+ Console.SetError( cstw );
+ }
+ private static bool hasConsole = false;
+ private static IntPtr conOut;
+ private static IntPtr oldOut;
+ [DllImport("kernel32.dll", SetLastError=true)]
+ private static extern bool AllocConsole();
+ [DllImport("kernel32.dll", SetLastError=false)]
+ private static extern bool FreeConsole();
+ [DllImport("kernel32.dll", SetLastError=true)]
+ private static extern IntPtr GetStdHandle( int nStdHandle );
+ [DllImport("kernel32.dll", SetLastError=true)]
+ private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput);
+ [DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
+ private static extern IntPtr CreateFile(
+ string fileName,
+ int desiredAccess,
+ int shareMode,
+ IntPtr securityAttributes,
+ int creationDisposition,
+ int flagsAndAttributes,
+ IntPtr templateFile );
+ [DllImport("kernel32.dll", ExactSpelling=true, SetLastError=true)]
+ private static extern bool CloseHandle(IntPtr handle);
+ }
+}
\ No newline at end of file
diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj
new file mode 100644
index 00000000..f07fc0af
--- /dev/null
+++ b/IPA.Injector/IPA.Injector.csproj
@@ -0,0 +1,91 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}
+ Library
+ Properties
+ IPA.Injector
+ IPA.Injector
+ v4.6
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Ionic.Zip.1.9.1.8\lib\Ionic.Zip.dll
+
+
+ ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+ False
+ ..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll
+ False
+
+
+
+
+
+
+
+
+
+
+ {5ad344f0-01a0-4ca8-92e5-9d095737744d}
+ IPA.Loader
+
+
+
+
+
+
+
+ Libraries\Included\0Harmony.dll
+ Always
+
+
+ Libraries\Mono\I18N.dll
+ Always
+
+
+ Libraries\Mono\I18N.West.dll
+ Always
+
+
+ Libraries\Mono\System.Runtime.Serialization.dll
+ Always
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs
new file mode 100644
index 00000000..8638e9de
--- /dev/null
+++ b/IPA.Injector/Injector.cs
@@ -0,0 +1,63 @@
+using IllusionInjector;
+using IllusionInjector.Logging;
+using System;
+using System.IO;
+using System.Reflection;
+using UnityEngine;
+using static IllusionPlugin.Logging.Logger;
+using Logger = IllusionInjector.Logging.Logger;
+
+namespace IPA.Injector
+{
+ public static class Injector
+ {
+ private static bool injected = false;
+ public static void Inject()
+ {
+ if (!injected)
+ {
+ injected = true;
+ AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader;
+ var bootstrapper = new GameObject("Bootstrapper").AddComponent();
+ bootstrapper.Destroyed += Bootstrapper_Destroyed;
+ }
+ }
+
+ private static string libsDir;
+ private static Assembly AssemblyLibLoader(object source, ResolveEventArgs e)
+ {
+ if (libsDir == null)
+ libsDir = Path.Combine(Environment.CurrentDirectory, "Libs");
+
+ var asmName = new AssemblyName(e.Name);
+ Log(Level.Debug, $"Resolving library {asmName}");
+
+ var testFilen = Path.Combine(libsDir, $"{asmName.Name}.{asmName.Version}.dll");
+ Log(Level.Debug, $"Looking for file {testFilen}");
+
+ if (File.Exists(testFilen))
+ return Assembly.LoadFile(testFilen);
+
+ Log(Level.Critical, $"Could not load library {asmName}");
+
+ return null;
+ }
+ private static void Log(Level lvl, string message)
+ { // multiple proxy methods to delay loading of assemblies until it's done
+ if (Logger.LogCreated)
+ AssemblyLibLoaderCallLogger(lvl, message);
+ else
+ if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0)
+ Console.WriteLine($"[{lvl}] {message}");
+ }
+ private static void AssemblyLibLoaderCallLogger(Level lvl, string message)
+ {
+ Logger.log.Log(lvl, message);
+ }
+
+ private static void Bootstrapper_Destroyed()
+ {
+ PluginComponent.Create();
+ }
+ }
+}
diff --git a/IPA.Injector/PostBuild.msbuild b/IPA.Injector/PostBuild.msbuild
new file mode 100644
index 00000000..20f1fccb
--- /dev/null
+++ b/IPA.Injector/PostBuild.msbuild
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IPA.Injector/Properties/AssemblyInfo.cs b/IPA.Injector/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..bae971d5
--- /dev/null
+++ b/IPA.Injector/Properties/AssemblyInfo.cs
@@ -0,0 +1,37 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("IPA.Injector")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("IPA.Injector")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("2a1af16b-27f1-46e0-9a95-181516bc1cb7")]
+[assembly: InternalsVisibleTo("IPA.Loader")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("3.9.0")]
+[assembly: AssemblyFileVersion("3.9.0")]
diff --git a/IPA.Injector/packages.config b/IPA.Injector/packages.config
new file mode 100644
index 00000000..71c37d6d
--- /dev/null
+++ b/IPA.Injector/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/IPA.Loader/BeatSaber/CompositeBSPlugin.cs b/IPA.Loader/BeatSaber/CompositeBSPlugin.cs
new file mode 100644
index 00000000..96d6ddf0
--- /dev/null
+++ b/IPA.Loader/BeatSaber/CompositeBSPlugin.cs
@@ -0,0 +1,97 @@
+using IllusionPlugin;
+using IllusionPlugin.BeatSaber;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using Logger = IllusionInjector.Logging.Logger;
+
+namespace IllusionInjector {
+ public class CompositeBSPlugin : IBeatSaberPlugin
+ {
+ IEnumerable plugins;
+
+ private delegate void CompositeCall(IBeatSaberPlugin plugin);
+
+ public CompositeBSPlugin(IEnumerable plugins) {
+ this.plugins = plugins;
+ }
+
+ public void OnApplicationStart() {
+ Invoke(plugin => plugin.OnApplicationStart());
+ }
+
+ public void OnApplicationQuit() {
+ Invoke(plugin => plugin.OnApplicationQuit());
+ }
+
+ public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) {
+ foreach (var plugin in plugins) {
+ try {
+ plugin.OnSceneLoaded(scene, sceneMode);
+ }
+ catch (Exception ex) {
+ Logger.log.Error($"{plugin.Name}: {ex}");
+ }
+ }
+ }
+
+ public void OnSceneUnloaded(Scene scene) {
+ foreach (var plugin in plugins) {
+ try {
+ plugin.OnSceneUnloaded(scene);
+ }
+ catch (Exception ex) {
+ Logger.log.Error($"{plugin.Name}: {ex}");
+ }
+ }
+ }
+
+ public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) {
+ foreach (var plugin in plugins) {
+ try {
+ plugin.OnActiveSceneChanged(prevScene, nextScene);
+ }
+ catch (Exception ex) {
+ Logger.log.Error($"{plugin.Name}: {ex}");
+ }
+ }
+ }
+
+ private void Invoke(CompositeCall callback) {
+ foreach (var plugin in plugins) {
+ try {
+ callback(plugin);
+ }
+ catch (Exception ex) {
+ Logger.log.Error($"{plugin.Name}: {ex}");
+ }
+ }
+ }
+
+ public void OnUpdate() {
+ Invoke(plugin => plugin.OnUpdate());
+ }
+
+ public void OnFixedUpdate() {
+ Invoke(plugin => plugin.OnFixedUpdate());
+ }
+
+ public string Name => throw new NotImplementedException();
+
+ public string Version => throw new NotImplementedException();
+
+ public Uri UpdateUri => throw new NotImplementedException();
+
+ public ModsaberModInfo ModInfo => throw new NotImplementedException();
+
+ public void OnLateUpdate() {
+ Invoke(plugin => {
+ if (plugin is IEnhancedBeatSaberPlugin)
+ ((IEnhancedBeatSaberPlugin) plugin).OnLateUpdate();
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj
new file mode 100644
index 00000000..fe8d387f
--- /dev/null
+++ b/IPA.Loader/IPA.Loader.csproj
@@ -0,0 +1,94 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {5AD344F0-01A0-4CA8-92E5-9D095737744D}
+ Library
+ Properties
+ IPA.Loader
+ IPA.Loader
+ v4.6
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ true
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+ true
+
+
+
+ ..\packages\Ionic.Zip.1.9.1.8\lib\Ionic.Zip.dll
+
+
+ ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll
+ False
+
+
+ ..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestModule.dll
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/IPA.Loader/IPA/CompositeIPAPlugin.cs b/IPA.Loader/IPA/CompositeIPAPlugin.cs
new file mode 100644
index 00000000..001677a6
--- /dev/null
+++ b/IPA.Loader/IPA/CompositeIPAPlugin.cs
@@ -0,0 +1,75 @@
+using IllusionPlugin;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+using Logger = IllusionInjector.Logging.Logger;
+
+namespace IllusionInjector {
+#pragma warning disable CS0618 // Type or member is obsolete
+ public class CompositeIPAPlugin : IPlugin
+ {
+ IEnumerable plugins;
+
+ private delegate void CompositeCall(IPlugin plugin);
+
+ public CompositeIPAPlugin(IEnumerable plugins) {
+ this.plugins = plugins;
+ }
+
+ public void OnApplicationStart() {
+ Invoke(plugin => plugin.OnApplicationStart());
+ }
+
+ public void OnApplicationQuit() {
+ Invoke(plugin => plugin.OnApplicationQuit());
+ }
+
+ private void Invoke(CompositeCall callback) {
+ foreach (var plugin in plugins) {
+ try {
+ callback(plugin);
+ }
+ catch (Exception ex) {
+ Logger.log.Error($"{plugin.Name}: {ex}");
+ }
+ }
+ }
+
+ public void OnUpdate() {
+ Invoke(plugin => plugin.OnUpdate());
+ }
+
+ public void OnFixedUpdate() {
+ Invoke(plugin => plugin.OnFixedUpdate());
+ }
+
+ public string Name {
+ get { throw new NotImplementedException(); }
+ }
+
+ public string Version {
+ get { throw new NotImplementedException(); }
+ }
+
+ public void OnLateUpdate() {
+ Invoke(plugin => {
+ if (plugin is IEnhancedBeatSaberPlugin)
+ ((IEnhancedBeatSaberPlugin) plugin).OnLateUpdate();
+ });
+ }
+
+ public void OnLevelWasLoaded(int level)
+ {
+ Invoke(plugin => plugin.OnLevelWasLoaded(level));
+ }
+
+ public void OnLevelWasInitialized(int level)
+ {
+ Invoke(plugin => plugin.OnLevelWasInitialized(level));
+ }
+ }
+#pragma warning restore CS0618 // Type or member is obsolete
+}
\ No newline at end of file
diff --git a/IPA.Loader/IllusionPlugin/BeatSaber/IBeatSaberPlugin.cs b/IPA.Loader/IllusionPlugin/BeatSaber/IBeatSaberPlugin.cs
new file mode 100644
index 00000000..a9e2753c
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/BeatSaber/IBeatSaberPlugin.cs
@@ -0,0 +1,71 @@
+using IllusionPlugin.BeatSaber;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using UnityEngine.SceneManagement;
+
+namespace IllusionPlugin
+{
+ ///
+ /// Interface for Beat Saber plugins. Every class that implements this will be loaded if the DLL is placed at
+ /// data/Managed/Plugins.
+ ///
+ public interface IBeatSaberPlugin
+ {
+
+ ///
+ /// Gets the name of the plugin.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the version of the plugin.
+ ///
+ string Version { get; }
+
+ ///
+ /// Gets the info for the Modsaber release of this plugin. Return null if there is no Modsaber release.
+ ///
+ ModsaberModInfo ModInfo { get; }
+
+ ///
+ /// Gets invoked when the application is started.
+ ///
+ void OnApplicationStart();
+
+ ///
+ /// Gets invoked when the application is closed.
+ ///
+ void OnApplicationQuit();
+
+ ///
+ /// Gets invoked on every graphic update.
+ ///
+ void OnUpdate();
+
+ ///
+ /// Gets invoked on ever physics update.
+ ///
+ void OnFixedUpdate();
+
+ ///
+ /// Gets invoked whenever a scene is loaded.
+ ///
+ /// The scene currently loaded
+ /// The type of loading
+ void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode);
+
+ ///
+ /// Gets invoked whenever a scene is unloaded
+ ///
+ /// The unloaded scene
+ void OnSceneUnloaded(Scene scene);
+
+ ///
+ /// Gets invoked whenever a scene is changed
+ ///
+ /// The Scene that was previously loaded
+ /// The Scene being loaded
+ void OnActiveSceneChanged(Scene prevScene, Scene nextScene);
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/BeatSaber/IEnhancedBeatSaberPlugin.cs b/IPA.Loader/IllusionPlugin/BeatSaber/IEnhancedBeatSaberPlugin.cs
new file mode 100644
index 00000000..dfbd4f9f
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/BeatSaber/IEnhancedBeatSaberPlugin.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace IllusionPlugin
+{
+ ///
+ /// An enhanced version of a standard BeatSaber plugin.
+ ///
+ public interface IEnhancedBeatSaberPlugin : IBeatSaberPlugin, IGenericEnhancedPlugin
+ {
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/BeatSaber/ModsaberModInfo.cs b/IPA.Loader/IllusionPlugin/BeatSaber/ModsaberModInfo.cs
new file mode 100644
index 00000000..6defe31c
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/BeatSaber/ModsaberModInfo.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionPlugin.BeatSaber
+{
+ ///
+ /// A class to provide information about a mod on ModSaber.ML
+ ///
+ public class ModsaberModInfo
+ {
+ ///
+ /// The name the mod uses on ModSaber as an identifier.
+ ///
+ public string InternalName { get; set; }
+
+ ///
+ /// The version of the currently installed mod. Used to compare to the version on ModSaber.
+ ///
+ public Version CurrentVersion { get; set; }
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/IGenericEnhancedPlugin.cs b/IPA.Loader/IllusionPlugin/IGenericEnhancedPlugin.cs
new file mode 100644
index 00000000..80327f5c
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/IGenericEnhancedPlugin.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionPlugin
+{
+ ///
+ /// A generic interface for the modification for enhanced plugins.
+ ///
+ public interface IGenericEnhancedPlugin
+ {
+ ///
+ /// Gets a list of executables this plugin should be excuted on (without the file ending)
+ ///
+ /// { "PlayClub", "PlayClubStudio" }
+ string[] Filter { get; }
+
+ ///
+ /// Called after Update.
+ ///
+ void OnLateUpdate();
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/IPA/IEnhancedPlugin.cs b/IPA.Loader/IllusionPlugin/IPA/IEnhancedPlugin.cs
new file mode 100644
index 00000000..ac925b2e
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/IPA/IEnhancedPlugin.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace IllusionPlugin
+{
+ ///
+ /// An enhanced version of the standard IPA plugin.
+ ///
+ [Obsolete("When building plugins for Beat Saber, use IEnhancedBeatSaberPlugin")]
+ public interface IEnhancedPlugin : IPlugin, IGenericEnhancedPlugin
+ {
+ }
+}
\ No newline at end of file
diff --git a/IPA.Loader/IllusionPlugin/IPA/IPlugin.cs b/IPA.Loader/IllusionPlugin/IPA/IPlugin.cs
new file mode 100644
index 00000000..0610da73
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/IPA/IPlugin.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace IllusionPlugin
+{
+ ///
+ /// Interface for generic Illusion unity plugins. Every class that implements this will be loaded if the DLL is placed at
+ /// data/Managed/Plugins.
+ ///
+ [Obsolete("When building plugins for Beat Saber, use IBeatSaberPlugin")]
+ public interface IPlugin
+ {
+
+ ///
+ /// Gets the name of the plugin.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the version of the plugin.
+ ///
+ string Version { get; }
+
+ ///
+ /// Gets invoked when the application is started.
+ ///
+ void OnApplicationStart();
+
+ ///
+ /// Gets invoked when the application is closed.
+ ///
+ void OnApplicationQuit();
+
+ ///
+ /// Gets invoked whenever a level is loaded.
+ ///
+ ///
+ void OnLevelWasLoaded(int level);
+
+ ///
+ /// Gets invoked after the first update cycle after a level was loaded.
+ ///
+ ///
+ void OnLevelWasInitialized(int level);
+
+ ///
+ /// Gets invoked on every graphic update.
+ ///
+ void OnUpdate();
+
+
+ ///
+ /// Gets invoked on ever physics update.
+ ///
+ void OnFixedUpdate();
+ }
+}
\ No newline at end of file
diff --git a/IPA.Loader/IllusionPlugin/IniFile.cs b/IPA.Loader/IllusionPlugin/IniFile.cs
new file mode 100644
index 00000000..b8f47bce
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/IniFile.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace IllusionPlugin
+{
+ ///
+ /// Create a New INI file to store or load data
+ ///
+ internal class IniFile
+ {
+ [DllImport("KERNEL32.DLL", EntryPoint = "GetPrivateProfileStringW",
+ SetLastError = true,
+ CharSet = CharSet.Unicode, ExactSpelling = true,
+ CallingConvention = CallingConvention.StdCall)]
+ private static extern int GetPrivateProfileString(
+ string lpSection,
+ string lpKey,
+ string lpDefault,
+ StringBuilder lpReturnString,
+ int nSize,
+ string lpFileName);
+
+ [DllImport("KERNEL32.DLL", EntryPoint = "WritePrivateProfileStringW",
+ SetLastError = true,
+ CharSet = CharSet.Unicode, ExactSpelling = true,
+ CallingConvention = CallingConvention.StdCall)]
+ private static extern int WritePrivateProfileString(
+ string lpSection,
+ string lpKey,
+ string lpValue,
+ string lpFileName);
+
+ /*private string _path = "";
+ public string Path
+ {
+ get
+ {
+ return _path;
+ }
+ set
+ {
+ if (!File.Exists(value))
+ File.WriteAllText(value, "", Encoding.Unicode);
+ _path = value;
+ }
+ }*/
+
+ private FileInfo _iniFileInfo;
+ public FileInfo IniFileInfo {
+ get => _iniFileInfo;
+ set {
+ _iniFileInfo = value;
+ if (_iniFileInfo.Exists) return;
+ _iniFileInfo.Directory?.Create();
+ _iniFileInfo.Create();
+ }
+ }
+
+ ///
+ /// INIFile Constructor.
+ ///
+ ///
+ public IniFile(string iniPath)
+ {
+ IniFileInfo = new FileInfo(iniPath);
+ //this.Path = INIPath;
+ }
+
+ ///
+ /// Write Data to the INI File
+ ///
+ ///
+ /// Section name
+ ///
+ /// Key Name
+ ///
+ /// Value Name
+ public void IniWriteValue(string Section, string Key, string Value)
+ {
+ WritePrivateProfileString(Section, Key, Value, IniFileInfo.FullName);
+ }
+
+ ///
+ /// Read Data Value From the Ini File
+ ///
+ ///
+ ///
+ ///
+ public string IniReadValue(string Section, string Key)
+ {
+ const int MAX_CHARS = 1023;
+ StringBuilder result = new StringBuilder(MAX_CHARS);
+ GetPrivateProfileString(Section, Key, "", result, MAX_CHARS, IniFileInfo.FullName);
+ return result.ToString();
+ }
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/Logging/LogPrinter.cs b/IPA.Loader/IllusionPlugin/Logging/LogPrinter.cs
new file mode 100644
index 00000000..441029e6
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/Logging/LogPrinter.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionPlugin.Logging
+{
+ ///
+ /// The log printer's base class.
+ ///
+ public abstract class LogPrinter
+ {
+ ///
+ /// Provides a filter for which log levels to allow through.
+ ///
+ public abstract Logger.LogLevel Filter { get; set; }
+ ///
+ /// Prints a provided message from a given log at the specified time.
+ ///
+ /// the log level
+ /// the time the message was composed
+ /// the name of the log that created this message
+ /// the message
+ public abstract void Print(Logger.Level level, DateTime time, string logName, string message);
+ ///
+ /// Called before the first print in a group. May be called multiple times.
+ /// Use this to create file handles and the like.
+ ///
+ public virtual void StartPrint() { }
+ ///
+ /// Called after the last print in a group. May be called multiple times.
+ /// Use this to dispose file handles and the like.
+ ///
+ public virtual void EndPrint() { }
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/Logging/Logger.cs b/IPA.Loader/IllusionPlugin/Logging/Logger.cs
new file mode 100644
index 00000000..befa4ccd
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/Logging/Logger.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionPlugin.Logging
+{
+ ///
+ /// The logger base class. Provides the format for console logs.
+ ///
+ public abstract class Logger
+ {
+ ///
+ /// The standard format for log messages.
+ ///
+ public static string LogFormat { get; protected internal set; } = "[{3} @ {2:HH:mm:ss} | {1}] {0}";
+
+ ///
+ /// An enum specifying the level of the message. Resembles Syslog.
+ ///
+ public enum Level : byte
+ {
+ ///
+ /// No associated level. These never get shown.
+ ///
+ None = 0,
+ ///
+ /// A debug message.
+ ///
+ Debug = 1,
+ ///
+ /// An informational message.
+ ///
+ Info = 2,
+ ///
+ /// A warning message.
+ ///
+ Warning = 4,
+ ///
+ /// An error message.
+ ///
+ Error = 8,
+ ///
+ /// A critical error message.
+ ///
+ Critical = 16
+ }
+
+ ///
+ /// An enum providing log level filters.
+ ///
+ [Flags]
+ public enum LogLevel : byte
+ {
+ ///
+ /// Allow no messages through.
+ ///
+ None = Level.None,
+ ///
+ /// Only shows Debug messages.
+ ///
+ DebugOnly = Level.Debug,
+ ///
+ /// Only shows info messages.
+ ///
+ InfoOnly = Level.Info,
+ ///
+ /// Only shows Warning messages.
+ ///
+ WarningOnly = Level.Warning,
+ ///
+ /// Only shows Error messages.
+ ///
+ ErrorOnly = Level.Error,
+ ///
+ /// Only shows Critical messages.
+ ///
+ CriticalOnly = Level.Critical,
+
+ ///
+ /// Shows all messages error and up.
+ ///
+ ErrorUp = ErrorOnly | CriticalOnly,
+ ///
+ /// Shows all messages warning and up.
+ ///
+ WarningUp = WarningOnly | ErrorUp,
+ ///
+ /// Shows all messages info and up.
+ ///
+ InfoUp = InfoOnly | WarningUp,
+ ///
+ /// Shows all messages.
+ ///
+ All = DebugOnly | InfoUp,
+ }
+
+ ///
+ /// A basic log function.
+ ///
+ /// the level of the message
+ /// the message to log
+ public abstract void Log(Level level, string message);
+ ///
+ /// A basic log function taking an exception to log.
+ ///
+ /// the level of the message
+ /// the exception to log
+ public virtual void Log(Level level, Exception exeption) => Log(level, exeption.ToString());
+ ///
+ /// Sends a debug message.
+ /// Equivalent to Log(Level.Debug, message);
+ ///
+ ///
+ /// the message to log
+ public virtual void Debug(string message) => Log(Level.Debug, message);
+ ///
+ /// Sends an exception as a debug message.
+ /// Equivalent to Log(Level.Debug, e);
+ ///
+ ///
+ /// the exception to log
+ public virtual void Debug(Exception e) => Log(Level.Debug, e);
+ ///
+ /// Sends an info message.
+ /// Equivalent to Log(Level.Info, message).
+ ///
+ ///
+ /// the message to log
+ public virtual void Info(string message) => Log(Level.Info, message);
+ ///
+ /// Sends an exception as an info message.
+ /// Equivalent to Log(Level.Info, e);
+ ///
+ ///
+ /// the exception to log
+ public virtual void Info(Exception e) => Log(Level.Info, e);
+ ///
+ /// Sends a warning message.
+ /// Equivalent to Log(Level.Warning, message).
+ ///
+ ///
+ /// the message to log
+ public virtual void Warn(string message) => Log(Level.Warning, message);
+ ///
+ /// Sends an exception as a warning message.
+ /// Equivalent to Log(Level.Warning, e);
+ ///
+ ///
+ /// the exception to log
+ public virtual void Warn(Exception e) => Log(Level.Warning, e);
+ ///
+ /// Sends an error message.
+ /// Equivalent to Log(Level.Error, message).
+ ///
+ ///
+ /// the message to log
+ public virtual void Error(string message) => Log(Level.Error, message);
+ ///
+ /// Sends an exception as an error message.
+ /// Equivalent to Log(Level.Error, e);
+ ///
+ ///
+ /// the exception to log
+ public virtual void Error(Exception e) => Log(Level.Error, e);
+ ///
+ /// Sends a critical message.
+ /// Equivalent to Log(Level.Critical, message).
+ ///
+ ///
+ /// the message to log
+ public virtual void Critical(string message) => Log(Level.Critical, message);
+ ///
+ /// Sends an exception as a critical message.
+ /// Equivalent to Log(Level.Critical, e);
+ ///
+ ///
+ /// the exception to log
+ public virtual void Critical(Exception e) => Log(Level.Critical, e);
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/ModPrefs.cs b/IPA.Loader/IllusionPlugin/ModPrefs.cs
new file mode 100644
index 00000000..198cc6aa
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/ModPrefs.cs
@@ -0,0 +1,285 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace IllusionPlugin
+{
+ ///
+ /// Allows to get and set preferences for your mod.
+ ///
+ public interface IModPrefs
+ {
+ ///
+ /// Gets a string from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ string GetString(string section, string name, string defaultValue = "", bool autoSave = false);
+ ///
+ /// Gets an int from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ int GetInt(string section, string name, int defaultValue = 0, bool autoSave = false);
+ ///
+ /// Gets a float from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ float GetFloat(string section, string name, float defaultValue = 0f, bool autoSave = false);
+ ///
+ /// Gets a bool from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ bool GetBool(string section, string name, bool defaultValue = false, bool autoSave = false);
+ ///
+ /// Checks whether or not a key exists in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ ///
+ bool HasKey(string section, string name);
+ ///
+ /// Sets a float in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ void SetFloat(string section, string name, float value);
+ ///
+ /// Sets an int in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ void SetInt(string section, string name, int value);
+ ///
+ /// Sets a string in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ void SetString(string section, string name, string value);
+ ///
+ /// Sets a bool in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ void SetBool(string section, string name, bool value);
+ }
+
+ ///
+ /// Allows to get and set preferences for your mod.
+ ///
+ public class ModPrefs : IModPrefs
+ {
+ private static ModPrefs _staticInstance = null;
+ private static IModPrefs StaticInstace
+ {
+ get
+ {
+ if (_staticInstance == null)
+ _staticInstance = new ModPrefs();
+ return _staticInstance;
+ }
+ }
+
+ internal static Dictionary ModPrefses { get; set; } = new Dictionary();
+
+ private IniFile Instance;
+
+ ///
+ /// Constructs a ModPrefs object for the provide plugin.
+ ///
+ /// the plugin to get the preferences file for
+ public ModPrefs(IBeatSaberPlugin plugin) {
+ Instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs", $"{plugin.Name}.ini"));
+ ModPrefses.Add(plugin, this);
+ }
+
+ private ModPrefs()
+ {
+ Instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "modprefs.ini"));
+ }
+
+ string IModPrefs.GetString(string section, string name, string defaultValue, bool autoSave)
+ {
+ var value = Instance.IniReadValue(section, name);
+ if (value != "")
+ return value;
+ else if (autoSave)
+ (this as IModPrefs).SetString(section, name, defaultValue);
+
+ return defaultValue;
+ }
+ ///
+ /// Gets a string from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ public static string GetString(string section, string name, string defaultValue = "", bool autoSave = false)
+ => StaticInstace.GetString(section, name, defaultValue, autoSave);
+
+ int IModPrefs.GetInt(string section, string name, int defaultValue, bool autoSave)
+ {
+ if (int.TryParse(Instance.IniReadValue(section, name), out var value))
+ return value;
+ else if (autoSave)
+ (this as IModPrefs).SetInt(section, name, defaultValue);
+
+ return defaultValue;
+ }
+ ///
+ /// Gets an int from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ public static int GetInt(string section, string name, int defaultValue = 0, bool autoSave = false)
+ => StaticInstace.GetInt(section, name, defaultValue, autoSave);
+
+ float IModPrefs.GetFloat(string section, string name, float defaultValue, bool autoSave)
+ {
+ if (float.TryParse(Instance.IniReadValue(section, name), out var value))
+ return value;
+ else if (autoSave)
+ (this as IModPrefs).SetFloat(section, name, defaultValue);
+
+ return defaultValue;
+ }
+ ///
+ /// Gets a float from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ public static float GetFloat(string section, string name, float defaultValue = 0f, bool autoSave = false)
+ => StaticInstace.GetFloat(section, name, defaultValue, autoSave);
+
+ bool IModPrefs.GetBool(string section, string name, bool defaultValue, bool autoSave)
+ {
+ string sVal = GetString(section, name, null);
+ if (sVal == "1" || sVal == "0")
+ {
+ return sVal == "1";
+ } else if (autoSave)
+ {
+ (this as IModPrefs).SetBool(section, name, defaultValue);
+ }
+
+ return defaultValue;
+ }
+ ///
+ /// Gets a bool from the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be used when no value is found.
+ /// Whether or not the default value should be written if no value is found.
+ ///
+ public static bool GetBool(string section, string name, bool defaultValue = false, bool autoSave = false)
+ => StaticInstace.GetBool(section, name, defaultValue, autoSave);
+
+ bool IModPrefs.HasKey(string section, string name)
+ {
+ return Instance.IniReadValue(section, name) != null;
+ }
+ ///
+ /// Checks whether or not a key exists in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ ///
+ public static bool HasKey(string section, string name) => StaticInstace.HasKey(section, name);
+
+ void IModPrefs.SetFloat(string section, string name, float value)
+ {
+ Instance.IniWriteValue(section, name, value.ToString());
+ }
+ ///
+ /// Sets a float in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ public static void SetFloat(string section, string name, float value)
+ => StaticInstace.SetFloat(section, name, value);
+
+ void IModPrefs.SetInt(string section, string name, int value)
+ {
+ Instance.IniWriteValue(section, name, value.ToString());
+ }
+ ///
+ /// Sets an int in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ public static void SetInt(string section, string name, int value)
+ => StaticInstace.SetInt(section, name, value);
+
+ void IModPrefs.SetString(string section, string name, string value)
+ {
+ Instance.IniWriteValue(section, name, value);
+ }
+ ///
+ /// Sets a string in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ public static void SetString(string section, string name, string value)
+ => StaticInstace.SetString(section, name, value);
+
+ void IModPrefs.SetBool(string section, string name, bool value)
+ {
+ Instance.IniWriteValue(section, name, value ? "1" : "0");
+ }
+ ///
+ /// Sets a bool in the ini.
+ ///
+ /// Section of the key.
+ /// Name of the key.
+ /// Value that should be written.
+ public static void SetBool(string section, string name, bool value)
+ => StaticInstace.SetBool(section, name, value);
+ }
+
+ ///
+ /// An extension class for IBeatSaberPlugins.
+ ///
+ public static class ModPrefsExtensions {
+ ///
+ /// Gets the ModPrefs object for the provided plugin.
+ ///
+ /// the plugin wanting the prefrences
+ /// the ModPrefs object
+ public static IModPrefs GetModPrefs(this IBeatSaberPlugin plugin) {
+ return ModPrefs.ModPrefses.First(o => o.Key == plugin).Value;
+ }
+ }
+}
diff --git a/IPA.Loader/IllusionPlugin/Utils/ReflectionUtil.cs b/IPA.Loader/IllusionPlugin/Utils/ReflectionUtil.cs
new file mode 100644
index 00000000..cf8fdde1
--- /dev/null
+++ b/IPA.Loader/IllusionPlugin/Utils/ReflectionUtil.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Reflection;
+using UnityEngine;
+
+namespace IllusionPlugin.Utils
+{
+ ///
+ /// A utility class providing reflection helper methods.
+ ///
+ public static class ReflectionUtil
+ {
+ ///
+ /// Sets a (potentially) private field on the target object.
+ ///
+ /// the object instance
+ /// the field to set
+ /// the value to set it to
+ public static void SetPrivateField(this object obj, string fieldName, object value)
+ {
+ var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
+ prop.SetValue(obj, value);
+ }
+
+ ///
+ /// Gets the value of a (potentially) private field.
+ ///
+ /// the type of te field (result casted)
+ /// the object instance to pull from
+ /// the name of the field to read
+ /// the value of the field
+ public static T GetPrivateField(this object obj, string fieldName)
+ {
+ var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
+ var value = prop.GetValue(obj);
+ return (T) value;
+ }
+
+ ///
+ /// Sets a (potentially) private propert on the target object.
+ ///
+ /// the target object instance
+ /// the name of the property
+ /// the value to set it to
+ public static void SetPrivateProperty(this object obj, string propertyName, object value)
+ {
+ var prop = obj.GetType()
+ .GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
+ prop.SetValue(obj, value, null);
+ }
+
+ ///
+ /// Invokes a (potentially) private method.
+ ///
+ /// the object to call from
+ /// the method name
+ /// the method parameters
+ /// the return value
+ public static object InvokePrivateMethod(this object obj, string methodName, params object[] methodParams)
+ {
+ MethodInfo dynMethod = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
+ return dynMethod.Invoke(obj, methodParams);
+ }
+
+ ///
+ /// Invokes a (potentially) private method.
+ ///
+ /// the return type
+ /// the object to call from
+ /// the method name to call
+ /// the method's parameters
+ /// the return value
+ public static T InvokePrivateMethod(this object obj, string methodName, params object[] methodParams)
+ {
+ return (T)InvokePrivateMethod(obj, methodName, methodParams);
+ }
+
+ ///
+ /// Copies a component of type originalType to a component of overridingType on the destination GameObject.
+ ///
+ /// the original component
+ /// the new component's type
+ /// the destination GameObject
+ /// overrides the source component type (for example, to a superclass)
+ /// the copied component
+ public static Component CopyComponent(this Component original, Type overridingType, GameObject destination, Type originalTypeOverride = null)
+ {
+ var copy = destination.AddComponent(overridingType);
+ var originalType = originalTypeOverride ?? original.GetType();
+
+ Type type = originalType;
+ while (type != typeof(MonoBehaviour))
+ {
+ CopyForType(type, original, copy);
+ type = type.BaseType;
+ }
+
+ return copy;
+ }
+
+ ///
+ /// A generic version of CopyComponent.
+ ///
+ ///
+ /// the overriding type
+ /// the original component
+ /// the destination game object
+ /// overrides the source component type (for example, to a superclass)
+ /// the copied component
+ public static T CopyComponent(this Component original, GameObject destination, Type originalTypeOverride = null)
+ where T : Component
+ {
+ var copy = destination.AddComponent();
+ var originalType = originalTypeOverride ?? original.GetType();
+
+ Type type = originalType;
+ while (type != typeof(MonoBehaviour))
+ {
+ CopyForType(type, original, copy);
+ type = type.BaseType;
+ }
+
+ return copy;
+ }
+
+ private static void CopyForType(Type type, Component source, Component destination)
+ {
+ FieldInfo[] myObjectFields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetField);
+
+ foreach (FieldInfo fi in myObjectFields)
+ {
+ fi.SetValue(destination, fi.GetValue(source));
+ }
+ }
+
+ ///
+ /// Calls an instance method on a type specified by functionClass and dependency.
+ ///
+ ///
+ /// the type name
+ /// the assembly the type is in
+ /// the name of the method to call
+ /// the type signature of the method
+ /// the method parameters
+ /// the result of the call
+ public static object CallNonStaticMethod(string functionClass, string dependency, string function, Type[] methodSig, params object[] parameters)
+ {
+ return CallNonStaticMethod(Type.GetType(string.Format("{0},{1}", functionClass, dependency)), function, methodSig, parameters);
+ }
+
+ ///
+ /// Calls an instance method on a new object.
+ ///
+ /// the object type
+ /// the name of the method to call
+ /// the type signature
+ /// the parameters
+ /// the result of the call
+ public static object CallNonStaticMethod(this Type type, /*string functionClass, string dependency,*/ string function, Type[] methodSig, params object[] parameters)
+ {
+ //Type FunctionClass = Type.GetType(string.Format("{0},{1}", functionClass, dependency));
+ if (type != null)
+ {
+ object instance = Activator.CreateInstance(type);
+ if (instance != null)
+ {
+ Type instType = instance.GetType();
+ MethodInfo methodInfo = instType.GetMethod(function, methodSig);
+ if (methodInfo != null)
+ {
+ return methodInfo.Invoke(instance, parameters);
+ }
+ else
+ {
+ throw new Exception("Method not found");
+ }
+ }
+ else
+ {
+ throw new Exception("Unable to instantiate object of type");
+ }
+ }
+ else
+ {
+ throw new ArgumentNullException("type");
+ }
+ }
+
+ ///
+ /// Calls an instance method on a new object.
+ ///
+ ///
+ /// the return type
+ /// the object type
+ /// the name of the method to call
+ /// the type signature
+ /// the parameters
+ /// the result of the call
+ public static T CallNonStaticMethod(this Type type, string function, Type[] methodSig, params object[] parameters)
+ {
+ return (T)CallNonStaticMethod(type, function, methodSig, parameters);
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs b/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs
new file mode 100644
index 00000000..a4759b28
--- /dev/null
+++ b/IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using IllusionPlugin.Logging;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector.Logging.Printers
+{
+ public class ColoredConsolePrinter : LogPrinter
+ {
+ LoggerBase.LogLevel filter = LoggerBase.LogLevel.All;
+ public override LoggerBase.LogLevel Filter { get => filter; set => filter = value; }
+
+ ConsoleColor color = Console.ForegroundColor;
+ public ConsoleColor Color { get => color; set => color = value; }
+
+ public override void Print(LoggerBase.Level level, DateTime time, string logName, string message)
+ {
+ if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return;
+ Console.ForegroundColor = color;
+ foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
+ Console.WriteLine(string.Format(LoggerBase.LogFormat, line, logName, time, level.ToString().ToUpper()));
+ Console.ResetColor();
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/Printers/GZFilePrinter.cs b/IPA.Loader/Logging/Printers/GZFilePrinter.cs
new file mode 100644
index 00000000..7a3fac36
--- /dev/null
+++ b/IPA.Loader/Logging/Printers/GZFilePrinter.cs
@@ -0,0 +1,92 @@
+using IllusionPlugin.Logging;
+using Ionic.Zlib;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IllusionInjector.Logging.Printers
+{
+ public abstract class GZFilePrinter : LogPrinter
+ {
+ [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ static extern bool CreateHardLink(
+ string lpFileName,
+ string lpExistingFileName,
+ IntPtr lpSecurityAttributes
+ );
+
+ [DllImport("Kernel32.dll")]
+ static extern Int32 GetLastError();
+
+ private FileInfo fileInfo;
+ protected StreamWriter fileWriter;
+ private GZipStream zstream;
+ private FileStream fstream;
+
+ protected abstract FileInfo GetFileInfo();
+
+ private void InitLog()
+ {
+ try
+ {
+ if (fileInfo == null)
+ { // first init
+ fileInfo = GetFileInfo();
+ var ext = fileInfo.Extension;
+ fileInfo = new FileInfo(fileInfo.FullName + ".gz");
+ fileInfo.Create().Close();
+
+ var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName, $"latest{ext}.gz"));
+ if (symlink.Exists) symlink.Delete();
+
+ try
+ {
+ if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero))
+ {
+ Logger.log.Error($"Hardlink creation failed {GetLastError()}");
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.log.Error("Error creating latest hardlink!");
+ Logger.log.Error(e);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.log.Error("Error initializing log!");
+ Logger.log.Error(e);
+ }
+ }
+
+ public override sealed void StartPrint()
+ {
+ InitLog();
+
+ fstream = fileInfo.Open(FileMode.Append, FileAccess.Write);
+ zstream = new GZipStream(fstream, CompressionMode.Compress)
+ {
+ FlushMode = FlushType.Full
+ };
+ fileWriter = new StreamWriter(zstream, new UTF8Encoding(false));
+ }
+
+ public override sealed void EndPrint()
+ {
+ fileWriter.Flush();
+ zstream.Flush();
+ fstream.Flush();
+ fileWriter.Close();
+ zstream.Close();
+ fstream.Close();
+ fileWriter.Dispose();
+ zstream.Dispose();
+ fstream.Dispose();
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs
new file mode 100644
index 00000000..9f617b72
--- /dev/null
+++ b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using IllusionPlugin.Logging;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector.Logging.Printers
+{
+ class GlobalLogFilePrinter : GZFilePrinter
+ {
+ public override LoggerBase.LogLevel Filter { get; set; } = LoggerBase.LogLevel.All;
+
+ public override void Print(IllusionPlugin.Logging.Logger.Level level, DateTime time, string logName, string message)
+ {
+ foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
+ fileWriter.WriteLine(string.Format(LoggerBase.LogFormat, line, logName, time, level.ToString().ToUpper()));
+ }
+
+ protected override FileInfo GetFileInfo()
+ {
+ var logsDir = new DirectoryInfo("Logs");
+ logsDir.Create();
+ var finfo = new FileInfo(Path.Combine(logsDir.FullName, $"{DateTime.Now:yyyy.MM.dd.HH.mm}.log"));
+ return finfo;
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs
new file mode 100644
index 00000000..cd0c41e4
--- /dev/null
+++ b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs
@@ -0,0 +1,37 @@
+using IllusionPlugin.Logging;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector.Logging.Printers
+{
+ class PluginLogFilePrinter : GZFilePrinter
+ {
+ public override LoggerBase.LogLevel Filter { get; set; } = LoggerBase.LogLevel.All;
+
+ private string name;
+
+ protected override FileInfo GetFileInfo()
+ {
+ var logsDir = new DirectoryInfo(Path.Combine("Logs",name));
+ logsDir.Create();
+ var finfo = new FileInfo(Path.Combine(logsDir.FullName, $"{DateTime.Now:yyyy.MM.dd.HH.mm}.log"));
+ return finfo;
+ }
+
+ public PluginLogFilePrinter(string name)
+ {
+ this.name = name;
+ }
+
+ public override void Print(IllusionPlugin.Logging.Logger.Level level, DateTime time, string logName, string message)
+ {
+ foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
+ fileWriter.WriteLine(string.Format("[{3} @ {2:HH:mm:ss}] {0}", line, logName, time, level.ToString().ToUpper()));
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs
new file mode 100644
index 00000000..c2c9a484
--- /dev/null
+++ b/IPA.Loader/Logging/StandardLogger.cs
@@ -0,0 +1,168 @@
+using IllusionInjector.Logging.Printers;
+using IllusionPlugin;
+using IllusionPlugin.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector.Logging
+{
+ internal static class Logger
+ {
+ private static LoggerBase _log;
+ internal static LoggerBase log
+ {
+ get
+ {
+ if (_log == null)
+ _log = new StandardLogger("IPA");
+ return _log;
+ }
+ }
+ internal static bool LogCreated => _log != null;
+ }
+
+ public class StandardLogger : LoggerBase
+ {
+ private static readonly IReadOnlyList defaultPrinters = new List()
+ {
+ new ColoredConsolePrinter()
+ {
+ Filter = LogLevel.DebugOnly,
+ Color = ConsoleColor.Green,
+ },
+ new ColoredConsolePrinter()
+ {
+ Filter = LogLevel.InfoOnly,
+ Color = ConsoleColor.White,
+ },
+ new ColoredConsolePrinter()
+ {
+ Filter = LogLevel.WarningOnly,
+ Color = ConsoleColor.Yellow,
+ },
+ new ColoredConsolePrinter()
+ {
+ Filter = LogLevel.ErrorOnly,
+ Color = ConsoleColor.Red,
+ },
+ new ColoredConsolePrinter()
+ {
+ Filter = LogLevel.CriticalOnly,
+ Color = ConsoleColor.Magenta,
+ },
+ new GlobalLogFilePrinter()
+ };
+
+ private string logName;
+ private static bool showSourceClass = true;
+ public static LogLevel PrintFilter { get; set; } = LogLevel.InfoUp;
+ private List printers = new List(defaultPrinters);
+
+ static StandardLogger()
+ {
+ if (ModPrefs.GetBool("IPA", "PrintDebug", false, true))
+ PrintFilter = LogLevel.All;
+ showSourceClass = ModPrefs.GetBool("IPA", "DebugShowCallSource", false, true);
+ }
+
+ internal StandardLogger(string name)
+ {
+ logName = name;
+
+ printers.Add(new PluginLogFilePrinter(name));
+
+ if (_logThread == null || !_logThread.IsAlive)
+ {
+ _logThread = new Thread(LogThread);
+ _logThread.Start();
+ }
+ }
+
+ public override void Log(Level level, string message)
+ {
+ _logQueue.Add(new LogMessage
+ {
+ level = level,
+ message = message,
+ logger = this,
+ time = DateTime.Now
+ });
+ }
+
+ public override void Debug(string message)
+ { // add source to message
+ var stfm = new StackTrace().GetFrame(1).GetMethod();
+ if (showSourceClass)
+ base.Debug($"{{{stfm.DeclaringType.FullName}::{stfm.Name}}} {message}");
+ else
+ base.Debug(message);
+ }
+
+ internal struct LogMessage
+ {
+ public Level level;
+ public StandardLogger logger;
+ public string message;
+ public DateTime time;
+ }
+
+ private static BlockingCollection _logQueue = new BlockingCollection();
+ private static Thread _logThread;
+
+ private static void LogThread()
+ {
+ HashSet started = new HashSet();
+ while (_logQueue.TryTake(out LogMessage msg, Timeout.Infinite)) {
+ foreach (var printer in msg.logger.printers)
+ {
+ try
+ {
+ if (((byte)msg.level & (byte)printer.Filter) != 0)
+ {
+ if (!started.Contains(printer))
+ {
+ printer.StartPrint();
+ started.Add(printer);
+ }
+
+ printer.Print(msg.level, msg.time, msg.logger.logName, msg.message);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"printer errored {e}");
+ }
+ }
+
+ if (_logQueue.Count == 0)
+ {
+ foreach (var printer in started)
+ {
+ try
+ {
+ printer.EndPrint();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"printer errored {e}");
+ }
+ }
+ started.Clear();
+ }
+ }
+ }
+
+ public static void StopLogThread()
+ {
+ _logQueue.CompleteAdding();
+ _logThread.Join();
+ }
+ }
+}
diff --git a/IPA.Loader/Logging/UnityLogInterceptor.cs b/IPA.Loader/Logging/UnityLogInterceptor.cs
new file mode 100644
index 00000000..64e42926
--- /dev/null
+++ b/IPA.Loader/Logging/UnityLogInterceptor.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using UnityEngine;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector.Logging
+{
+ public class UnityLogInterceptor
+ {
+ public static LoggerBase Unitylogger = new StandardLogger("UnityEngine");
+
+ public static LoggerBase.Level LogTypeToLevel(LogType type)
+ {
+ switch (type)
+ {
+ case LogType.Assert:
+ return LoggerBase.Level.Debug;
+ case LogType.Error:
+ return LoggerBase.Level.Error;
+ case LogType.Exception:
+ return LoggerBase.Level.Critical;
+ case LogType.Log:
+ return LoggerBase.Level.Info;
+ case LogType.Warning:
+ return LoggerBase.Level.Warning;
+ default:
+ return LoggerBase.Level.Info;
+ }
+ }
+ }
+}
diff --git a/IPA.Loader/PluginComponent.cs b/IPA.Loader/PluginComponent.cs
new file mode 100644
index 00000000..1f3fc9d3
--- /dev/null
+++ b/IPA.Loader/PluginComponent.cs
@@ -0,0 +1,109 @@
+using IllusionInjector.Logging;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+namespace IllusionInjector
+{
+ public class PluginComponent : MonoBehaviour
+ {
+ private CompositeBSPlugin bsPlugins;
+ private CompositeIPAPlugin ipaPlugins;
+ private bool quitting = false;
+
+ public static PluginComponent Create()
+ {
+ Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type)
+ {
+ var level = UnityLogInterceptor.LogTypeToLevel(type);
+ UnityLogInterceptor.Unitylogger.Log(level, $"{condition.Trim()}");
+ UnityLogInterceptor.Unitylogger.Log(level, $"{stackTrace.Trim()}");
+ };
+
+ return new GameObject("IPA_PluginManager").AddComponent();
+ }
+
+ void Awake()
+ {
+ DontDestroyOnLoad(gameObject);
+
+ bsPlugins = new CompositeBSPlugin(PluginManager.BSPlugins);
+ ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins);
+
+ // this has no relevance since there is a new mod updater system
+ //gameObject.AddComponent(); // AFTER plugins are loaded, but before most things
+ gameObject.AddComponent();
+
+ bsPlugins.OnApplicationStart();
+ ipaPlugins.OnApplicationStart();
+
+ SceneManager.activeSceneChanged += OnActiveSceneChanged;
+ SceneManager.sceneLoaded += OnSceneLoaded;
+ SceneManager.sceneUnloaded += OnSceneUnloaded;
+ }
+
+ void Update()
+ {
+ bsPlugins.OnUpdate();
+ ipaPlugins.OnUpdate();
+ }
+
+ void LateUpdate()
+ {
+ bsPlugins.OnLateUpdate();
+ ipaPlugins.OnLateUpdate();
+ }
+
+ void FixedUpdate()
+ {
+ bsPlugins.OnFixedUpdate();
+ ipaPlugins.OnFixedUpdate();
+ }
+
+ void OnDestroy()
+ {
+ if (!quitting)
+ {
+ Create();
+ }
+ }
+
+ void OnApplicationQuit()
+ {
+ SceneManager.activeSceneChanged -= OnActiveSceneChanged;
+ SceneManager.sceneLoaded -= OnSceneLoaded;
+ SceneManager.sceneUnloaded -= OnSceneUnloaded;
+
+ bsPlugins.OnApplicationQuit();
+ ipaPlugins.OnApplicationQuit();
+
+ quitting = true;
+ }
+
+ void OnLevelWasLoaded(int level)
+ {
+ ipaPlugins.OnLevelWasLoaded(level);
+ }
+
+ public void OnLevelWasInitialized(int level)
+ {
+ ipaPlugins.OnLevelWasInitialized(level);
+ }
+
+ void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode)
+ {
+ bsPlugins.OnSceneLoaded(scene, sceneMode);
+ }
+
+ private void OnSceneUnloaded(Scene scene) {
+ bsPlugins.OnSceneUnloaded(scene);
+ }
+
+ private void OnActiveSceneChanged(Scene prevScene, Scene nextScene) {
+ bsPlugins.OnActiveSceneChanged(prevScene, nextScene);
+ }
+
+ }
+}
diff --git a/IPA.Loader/PluginManager.cs b/IPA.Loader/PluginManager.cs
new file mode 100644
index 00000000..14c67222
--- /dev/null
+++ b/IPA.Loader/PluginManager.cs
@@ -0,0 +1,264 @@
+using IllusionInjector.Logging;
+using IllusionInjector.Updating;
+using IllusionInjector.Utilities;
+using IllusionPlugin;
+using IllusionPlugin.BeatSaber;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using LoggerBase = IllusionPlugin.Logging.Logger;
+
+namespace IllusionInjector
+{
+ public static class PluginManager
+ {
+#pragma warning disable CS0618 // Type or member is obsolete (IPlugin)
+
+ public class BSPluginMeta
+ {
+ public IBeatSaberPlugin Plugin { get; internal set; }
+ public string Filename { get; internal set; }
+ public ModsaberModInfo ModsaberInfo { get; internal set; }
+ }
+
+ public static IEnumerable BSPlugins
+ {
+ get
+ {
+ if(_bsPlugins == null)
+ {
+ LoadPlugins();
+ }
+ return _bsPlugins.Select(p => p.Plugin);
+ }
+ }
+ private static List _bsPlugins = null;
+ internal static IEnumerable BSMetas
+ {
+ get
+ {
+ if (_bsPlugins == null)
+ {
+ LoadPlugins();
+ }
+ return _bsPlugins;
+ }
+ }
+
+ public static IEnumerable Plugins
+ {
+ get
+ {
+ if (_ipaPlugins == null)
+ {
+ LoadPlugins();
+ }
+ return _ipaPlugins;
+ }
+ }
+ private static List _ipaPlugins = null;
+
+
+
+ private static void LoadPlugins()
+ {
+ string pluginDirectory = Path.Combine(Environment.CurrentDirectory, "Plugins");
+
+ // 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);
+ Logger.log.Info(exeName);
+ _bsPlugins = new List();
+ _ipaPlugins = new List();
+
+ if (!Directory.Exists(pluginDirectory)) return;
+
+ string cacheDir = Path.Combine(pluginDirectory, ".cache");
+
+ if (!Directory.Exists(cacheDir))
+ {
+ Directory.CreateDirectory(cacheDir);
+ }
+ else
+ {
+ foreach (string plugin in Directory.GetFiles(cacheDir, "*"))
+ {
+ File.Delete(plugin);
+ }
+ }
+
+ //Copy plugins to .cache
+ string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll");
+ foreach (string s in originalPlugins)
+ {
+ string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s));
+ File.Copy(Path.Combine(pluginDirectory, s), pluginCopy);
+ }
+
+ var selfPlugin = new BSPluginMeta
+ {
+ Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"),
+ Plugin = new SelfPlugin()
+ };
+ selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo;
+
+ _bsPlugins.Add(selfPlugin);
+
+ //Load copied plugins
+ string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll");
+ foreach (string s in copiedPlugins)
+ {
+ var result = LoadPluginsFromFile(s, exeName);
+ _bsPlugins.AddRange(result.Item1);
+ _ipaPlugins.AddRange(result.Item2);
+ }
+
+
+ // DEBUG
+ Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}");
+ Logger.log.Info($"Game version {UnityEngine.Application.version}");
+ Logger.log.Info("-----------------------------");
+ Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}");
+ Logger.log.Info("-----------------------------");
+ foreach (var plugin in _bsPlugins)
+ {
+ Logger.log.Info($"{plugin.Plugin.Name}: {plugin.Plugin.Version}");
+ }
+ Logger.log.Info("-----------------------------");
+ foreach (var plugin in _ipaPlugins)
+ {
+ Logger.log.Info($"{plugin.Name}: {plugin.Version}");
+ }
+ Logger.log.Info("-----------------------------");
+ }
+
+ private static Tuple, IEnumerable> LoadPluginsFromFile(string file, string exeName)
+ {
+ List bsPlugins = new List();
+ List ipaPlugins = new List();
+
+ if (!File.Exists(file) || !file.EndsWith(".dll", true, null))
+ return new Tuple, IEnumerable>(bsPlugins, ipaPlugins);
+
+ T OptionalGetPlugin(Type t) where T : class
+ {
+ // use typeof() to allow for easier renaming (in an ideal world this compiles to a string, but ¯\_(ツ)_/¯)
+ if (t.GetInterface(typeof(T).Name) != null)
+ {
+ try
+ {
+ T pluginInstance = Activator.CreateInstance(t) as T;
+ string[] filter = null;
+
+ if (pluginInstance is IGenericEnhancedPlugin)
+ {
+ filter = ((IGenericEnhancedPlugin)pluginInstance).Filter;
+ }
+
+ if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase))
+ return pluginInstance;
+ }
+ catch (Exception e)
+ {
+ Logger.log.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}");
+ }
+ }
+
+ return null;
+ }
+
+ try
+ {
+ Assembly assembly = Assembly.LoadFrom(file);
+
+ foreach (Type t in assembly.GetTypes())
+ {
+ IBeatSaberPlugin bsPlugin = OptionalGetPlugin(t);
+ if (bsPlugin != null)
+ {
+ try
+ {
+ var init = t.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
+ if (init != null)
+ {
+ var initArgs = new List