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(); + var initParams = init.GetParameters(); + + LoggerBase modLogger = null; + IModPrefs modPrefs = null; + + foreach (var param in initParams) + { + var ptype = param.ParameterType; + if (ptype.IsAssignableFrom(typeof(LoggerBase))) { + if (modLogger == null) modLogger = new StandardLogger(bsPlugin.Name); + initArgs.Add(modLogger); + } + else if (ptype.IsAssignableFrom(typeof(IModPrefs))) + { + if (modPrefs == null) modPrefs = new ModPrefs(bsPlugin); + initArgs.Add(modPrefs); + } + else + initArgs.Add(ptype.GetDefault()); + } + + init.Invoke(bsPlugin, initArgs.ToArray()); + } + + bsPlugins.Add(new BSPluginMeta + { + Plugin = bsPlugin, + Filename = file.Replace("\\.cache", ""), // quick and dirty fix + ModsaberInfo = bsPlugin.ModInfo + }); + } + catch (AmbiguousMatchException) + { + Logger.log.Error($"Only one Init allowed per plugin"); + } + } + else + { + IPlugin ipaPlugin = OptionalGetPlugin(t); + if (ipaPlugin != null) + { + ipaPlugins.Add(ipaPlugin); + } + } + } + + } + catch (Exception e) + { + Logger.log.Error($"Could not load {Path.GetFileName(file)}! {e}"); + } + + return new Tuple, IEnumerable>(bsPlugins, ipaPlugins); + } + + public class AppInfo + { + [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false)] + private static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length); + private static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); + public static string StartupPath + { + get + { + StringBuilder stringBuilder = new StringBuilder(260); + GetModuleFileName(NullHandleRef, stringBuilder, stringBuilder.Capacity); + return stringBuilder.ToString(); + } + } + } +#pragma warning restore CS0618 // Type or member is obsolete (IPlugin) + } +} diff --git a/IPA.Loader/Properties/AssemblyInfo.cs b/IPA.Loader/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a4c96091 --- /dev/null +++ b/IPA.Loader/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.Loader")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("IPA.Loader")] +[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("5ad344f0-01a0-4ca8-92e5-9d095737744d")] +[assembly: InternalsVisibleTo("IPA.Injector")] + +// 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("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs b/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs new file mode 100644 index 00000000..6465f381 --- /dev/null +++ b/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs @@ -0,0 +1,123 @@ +using IllusionInjector.Logging; +using IllusionInjector.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IllusionInjector.Updating.ModsaberML +{ + class ApiEndpoint + { +#if DEBUG && UPDATETEST + public const string ApiBase = "file://Z:/Users/aaron/Source/Repos/IPA-Reloaded-BeatSaber/IPA.Tests/"; + public const string GetApprovedEndpoint = "updater_test.json"; +#else + public const string ApiBase = "https://www.modsaber.ml/"; + public const string GetApprovedEndpoint = "registry/{0}"; +#endif + + class HexArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(byte[]); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + if (reader.TokenType == JsonToken.String) + { + try + { + return LoneFunctions.StringToByteArray((string)reader.Value); + } + catch (Exception ex) + { + throw new Exception(string.Format("Error parsing version string: {0}", reader.Value), ex); + } + } + throw new Exception(string.Format("Unexpected token or value when parsing hex string. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else + { + if (!(value is byte[])) + { + throw new JsonSerializationException("Expected byte[] object value"); + } + writer.WriteValue(LoneFunctions.ByteArrayToString(value as byte[])); + } + } + } + + [Serializable] + public class Mod + { +#pragma warning disable CS0649 + [JsonProperty("name")] + public string Name; + [JsonProperty("version"), + JsonConverter(typeof(VersionConverter))] + public Version Version; + [JsonProperty("approved")] + public bool Approved; + [JsonProperty("title")] + public string Title; + [JsonProperty("gameVersion"), + JsonConverter(typeof(VersionConverter))] + public Version GameVersion; + [JsonProperty("author")] + public string Author; +#pragma warning restore CS0649 + [Serializable] + public class PlatformFile + { + [JsonProperty("hash"), + JsonConverter(typeof(HexArrayConverter))] + public byte[] Hash = new byte[20]; + [JsonProperty("files", ItemConverterType = typeof(HexArrayConverter))] + public Dictionary FileHashes = new Dictionary(); + [JsonProperty("url")] + public string DownloadPath = null; + + public override string ToString() + { + return $"{LoneFunctions.ByteArrayToString(Hash)}@{DownloadPath}({string.Join(",",FileHashes.Select(o=>$"\"{o.Key}\":\"{LoneFunctions.ByteArrayToString(o.Value)}\""))})"; + } + } + + [Serializable] + public class FilesObject + { + [JsonProperty("steam")] + public PlatformFile Steam = null; + [JsonProperty("oculus")] + public PlatformFile Oculus = null; + } + + [JsonProperty("files")] + public FilesObject Files = null; + + public override string ToString() + { + return $"{{\"{Title} ({Name})\"v{Version} for {GameVersion} by {Author} with \"{Files.Steam}\" and \"{Files.Oculus}\"}}"; + } + } + + } +} diff --git a/IPA.Loader/Updating/ModsaberML/Updater.cs b/IPA.Loader/Updating/ModsaberML/Updater.cs new file mode 100644 index 00000000..26a5c653 --- /dev/null +++ b/IPA.Loader/Updating/ModsaberML/Updater.cs @@ -0,0 +1,346 @@ +using IllusionInjector.Updating.Backup; +using IllusionInjector.Utilities; +using Ionic.Zip; +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using UnityEngine.Networking; +using Logger = IllusionInjector.Logging.Logger; + +namespace IllusionInjector.Updating.ModsaberML +{ + class Updater : MonoBehaviour + { + public static Updater instance; + + public void Awake() + { + try + { + if (instance != null) + Destroy(this); + else + { + instance = this; + CheckForUpdates(); + } + } + catch (Exception e) + { + Logger.log.Error(e); + } + } + + public void CheckForUpdates() + { + StartCoroutine(CheckForUpdatesCoroutine()); + } + + private struct UpdateStruct + { + public PluginManager.BSPluginMeta plugin; + public ApiEndpoint.Mod externInfo; + } + + IEnumerator CheckForUpdatesCoroutine() + { + Logger.log.Info("Checking for mod updates..."); + + var toUpdate = new List(); + var GameVersion = new Version(Application.version); + + foreach (var plugin in PluginManager.BSMetas) + { + var info = plugin.ModsaberInfo; + if (info == null) continue; + + using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, info.InternalName))) + { + yield return request.SendWebRequest(); + + if (request.isNetworkError) + { + Logger.log.Error("Network error while trying to update mods"); + Logger.log.Error(request.error); + continue; + } + if (request.isHttpError) + { + if (request.responseCode == 404) + { + Logger.log.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}"); + continue; + } + + Logger.log.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}"); + Logger.log.Error(request.error); + continue; + } + + var json = request.downloadHandler.text; + + ApiEndpoint.Mod modRegistry; + try + { + modRegistry = JsonConvert.DeserializeObject(json); + Logger.log.Debug(modRegistry.ToString()); + } + catch (Exception e) + { + Logger.log.Error($"Parse error while trying to update mods"); + Logger.log.Error(e); + continue; + } + + Logger.log.Debug($"Found Modsaber.ML registration for {plugin.Plugin.Name} ({info.InternalName})"); + Logger.log.Debug($"Installed version: {info.CurrentVersion}; Latest version: {modRegistry.Version}"); + if (modRegistry.Version > info.CurrentVersion) + { + Logger.log.Debug($"{plugin.Plugin.Name} needs an update!"); + if (modRegistry.GameVersion == GameVersion) + { + Logger.log.Debug($"Queueing update..."); + toUpdate.Add(new UpdateStruct + { + plugin = plugin, + externInfo = modRegistry + }); + } + else + { + Logger.log.Warn($"Update avaliable for {plugin.Plugin.Name}, but for a different Beat Saber version!"); + } + } + } + } + + Logger.log.Info($"{toUpdate.Count} mods need updating"); + + if (toUpdate.Count == 0) yield break; + + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + foreach (var item in toUpdate) + { + StartCoroutine(UpdateModCoroutine(item, tempDirectory)); + } + } + + class StreamDownloadHandler : DownloadHandlerScript + { + public MemoryStream Stream { get; set; } + + public StreamDownloadHandler(MemoryStream stream) : base() + { + Stream = stream; + } + + protected override void ReceiveContentLength(int contentLength) + { + Stream.Capacity = contentLength; + Logger.log.Debug($"Got content length: {contentLength}"); + } + + protected override void CompleteContent() + { + Logger.log.Debug("Download complete"); + } + + protected override bool ReceiveData(byte[] data, int dataLength) + { + if (data == null || data.Length < 1) + { + Logger.log.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer"); + return false; + } + + Stream.Write(data, 0, dataLength); + return true; + } + + protected override byte[] GetData() { return null; } + + protected override float GetProgress() + { + return 0f; + } + + public override string ToString() + { + return $"{base.ToString()} ({Stream?.ToString()})"; + } + } + + private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory) + { + Logger.log.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}"); + + var data = stream.GetBuffer(); + SHA1 sha = new SHA1CryptoServiceProvider(); + var hash = sha.ComputeHash(data); + if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash)) + throw new Exception("The hash for the file doesn't match what is defined"); + + var newFiles = new List(); + var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}"); + + try + { + bool shouldDeleteOldFile = true; + + using (var zipFile = ZipFile.Read(stream)) + { + Logger.log.Debug("Streams opened"); + foreach (var entry in zipFile) + { + if (entry.IsDirectory) + { + Logger.log.Debug($"Creating directory {entry.FileName}"); + Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName)); + } + else + { + using (var ostream = new MemoryStream((int)entry.UncompressedSize)) + { + entry.Extract(ostream); + ostream.Seek(0, SeekOrigin.Begin); + + sha = new SHA1CryptoServiceProvider(); + var fileHash = sha.ComputeHash(ostream); + if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName])) + throw new Exception("The hash for the file doesn't match what is defined"); + + ostream.Seek(0, SeekOrigin.Begin); + FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName)); + Directory.CreateDirectory(targetFile.DirectoryName); + + if (targetFile.FullName == item.plugin.Filename) + shouldDeleteOldFile = false; // overwriting old file, no need to delete + + if (targetFile.Exists) + backup.Add(targetFile); + else + newFiles.Add(targetFile); + + Logger.log.Debug($"Extracting file {targetFile.FullName}"); + + var fstream = targetFile.Create(); + ostream.CopyTo(fstream); + } + } + } + } + + if (item.plugin.Plugin is SelfPlugin) + { // currently updating self + Process.Start(new ProcessStartInfo + { + FileName = item.plugin.Filename, + Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait", + UseShellExecute = false + }); + } + else if (shouldDeleteOldFile) + File.Delete(item.plugin.Filename); + } + catch (Exception) + { // something failed; restore + foreach (var file in newFiles) + file.Delete(); + backup.Restore(); + backup.Delete(); + + throw; + } + + backup.Delete(); + + Logger.log.Debug("Downloader exited"); + } + + IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory) + { + Logger.log.Debug($"Steam avaliable: {SteamCheck.IsAvailable}"); + + ApiEndpoint.Mod.PlatformFile platformFile; + if (SteamCheck.IsAvailable || item.externInfo.Files.Oculus == null) + platformFile = item.externInfo.Files.Steam; + else + platformFile = item.externInfo.Files.Oculus; + + string url = platformFile.DownloadPath; + + Logger.log.Debug($"URL = {url}"); + + const int MaxTries = 3; + int maxTries = MaxTries; + while (maxTries > 0) + { + if (maxTries-- != MaxTries) + Logger.log.Info($"Re-trying download..."); + + using (var stream = new MemoryStream()) + using (var request = UnityWebRequest.Get(url)) + using (var taskTokenSource = new CancellationTokenSource()) + { + var dlh = new StreamDownloadHandler(stream); + request.downloadHandler = dlh; + + Logger.log.Debug("Sending request"); + //Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL"); + yield return request.SendWebRequest(); + Logger.log.Debug("Download finished"); + + if (request.isNetworkError) + { + Logger.log.Error("Network error while trying to update mod"); + Logger.log.Error(request.error); + taskTokenSource.Cancel(); + continue; + } + if (request.isHttpError) + { + Logger.log.Error($"Server returned an error code while trying to update mod"); + Logger.log.Error(request.error); + taskTokenSource.Cancel(); + continue; + } + + stream.Seek(0, SeekOrigin.Begin); // reset to beginning + + var downloadTask = Task.Run(() => + { // use slightly more multithreaded approach than coroutines + ExtractPluginAsync(stream, item, platformFile, tempDirectory); + }, taskTokenSource.Token); + + while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted)) + yield return null; // pause coroutine until task is done + + if (downloadTask.IsFaulted) + { + Logger.log.Error($"Error downloading mod {item.plugin.Plugin.Name}"); + Logger.log.Error(downloadTask.Exception); + continue; + } + + break; + } + } + + if (maxTries == 0) + Logger.log.Warn($"Plugin download failed {MaxTries} times, not re-trying"); + else + Logger.log.Debug("Download complete"); + } + } +} diff --git a/IPA.Loader/Updating/SelfPlugin.cs b/IPA.Loader/Updating/SelfPlugin.cs new file mode 100644 index 00000000..b24f5642 --- /dev/null +++ b/IPA.Loader/Updating/SelfPlugin.cs @@ -0,0 +1,55 @@ +using IllusionPlugin; +using IllusionPlugin.BeatSaber; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine.SceneManagement; + +namespace IllusionInjector.Updating +{ + internal class SelfPlugin : IBeatSaberPlugin + { + internal const string IPA_Name = "Beat Saber IPA"; + internal const string IPA_Version = "3.9.0"; + + public string Name => IPA_Name; + + public string Version => IPA_Version; + + public ModsaberModInfo ModInfo => new ModsaberModInfo + { + CurrentVersion = new Version(IPA_Version), + InternalName = "beatsaber-ipa-reloaded" + }; + + public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) + { + } + + public void OnApplicationQuit() + { + } + + public void OnApplicationStart() + { + } + + public void OnFixedUpdate() + { + } + + public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) + { + } + + public void OnSceneUnloaded(Scene scene) + { + } + + public void OnUpdate() + { + } + } +} diff --git a/IPA.Loader/Utilities/Extensions.cs b/IPA.Loader/Utilities/Extensions.cs new file mode 100644 index 00000000..326c675a --- /dev/null +++ b/IPA.Loader/Utilities/Extensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IllusionInjector.Utilities +{ + public static class Extensions + { + public static object GetDefault(this Type type) + { + if (type.IsValueType) + { + return Activator.CreateInstance(type); + } + return null; + } + } +} diff --git a/IPA.Loader/Utilities/LoneFunctions.cs b/IPA.Loader/Utilities/LoneFunctions.cs new file mode 100644 index 00000000..c270f6e8 --- /dev/null +++ b/IPA.Loader/Utilities/LoneFunctions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IllusionInjector.Utilities +{ + public static class LoneFunctions + { + public static byte[] StringToByteArray(string hex) + { + int NumberChars = hex.Length; + byte[] bytes = new byte[NumberChars / 2]; + for (int i = 0; i < NumberChars; i += 2) + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + return bytes; + } + + public static string ByteArrayToString(byte[] ba) + { + StringBuilder hex = new StringBuilder(ba.Length * 2); + foreach (byte b in ba) + hex.AppendFormat("{0:x2}", b); + return hex.ToString(); + } + + // Copyright (c) 2008-2013 Hafthor Stefansson + // Distributed under the MIT/X11 software license + // Ref: http://www.opensource.org/licenses/mit-license.php. + // From: https://stackoverflow.com/a/8808245/3117125 + public static unsafe bool UnsafeCompare(byte[] a1, byte[] a2) + { + if (a1 == a2) return true; + if (a1 == null || a2 == null || a1.Length != a2.Length) + return false; + fixed (byte* p1 = a1, p2 = a2) + { + byte* x1 = p1, x2 = p2; + int l = a1.Length; + for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8) + if (*((long*)x1) != *((long*)x2)) return false; + if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; } + if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; } + if ((l & 1) != 0) if (*((byte*)x1) != *((byte*)x2)) return false; + return true; + } + } + + public static string GetRelativePath(string filespec, string folder) + { + Uri pathUri = new Uri(filespec); + // Folders must end in a slash + if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) + { + folder += Path.DirectorySeparatorChar; + } + Uri folderUri = new Uri(folder); + return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); + } + } +} diff --git a/IPA.Loader/Utilities/SteamCheck.cs b/IPA.Loader/Utilities/SteamCheck.cs new file mode 100644 index 00000000..a4f9624b --- /dev/null +++ b/IPA.Loader/Utilities/SteamCheck.cs @@ -0,0 +1,27 @@ +using IllusionInjector.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IllusionInjector.Utilities +{ + public static class SteamCheck + { + public static Type SteamVRCamera; + public static Type SteamVRExternalCamera; + public static Type SteamVRFade; + public static bool IsAvailable => FindSteamVRAsset(); + + private static bool FindSteamVRAsset() + { + // these require assembly qualified names.... + SteamVRCamera = Type.GetType("SteamVR_Camera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); + SteamVRExternalCamera = Type.GetType("SteamVR_ExternalCamera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); + SteamVRFade = Type.GetType("SteamVR_Fade, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); + + return SteamVRCamera != null && SteamVRExternalCamera != null && SteamVRFade != null; + } + } +} diff --git a/IPA.Loader/packages.config b/IPA.Loader/packages.config new file mode 100644 index 00000000..71c37d6d --- /dev/null +++ b/IPA.Loader/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/IPA/IPA.csproj b/IPA/IPA.csproj index f55672d3..1bb17d89 100644 --- a/IPA/IPA.csproj +++ b/IPA/IPA.csproj @@ -96,7 +96,7 @@ - + diff --git a/IPA/Patcher/Patcher.cs b/IPA/Patcher/Patcher.cs index 0a4527ce..7f8b25f5 100644 --- a/IPA/Patcher/Patcher.cs +++ b/IPA/Patcher/Patcher.cs @@ -51,8 +51,9 @@ namespace IPA.Patcher { var IIdata = new PatchData { IsPatched = false, Version = null }; foreach (var @ref in _Module.AssemblyReferences) { - if (@ref.Name == "IllusionInjector") IIdata = new PatchData { IsPatched = true, Version = new Version(0,0,0,0) }; - if (@ref.Name == "IllusionPlugin") return new PatchData { IsPatched = true, Version = @ref.Version }; + if (@ref.Name == "IllusionInjector") IIdata = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) }; + if (@ref.Name == "IllusionPlugin") IIdata = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) }; + if (@ref.Name == "IPA.Injector") return new PatchData { IsPatched = true, Version = @ref.Version }; } return IIdata; } @@ -61,9 +62,8 @@ namespace IPA.Patcher public void Patch(Version v) { // First, let's add the reference - var nameReference = new AssemblyNameReference("IllusionInjector", new Version(1,0,0,0)); - var versionNameReference = new AssemblyNameReference("IllusionPlugin", v); - var injectorPath = Path.Combine(_File.DirectoryName, "IllusionInjector.dll"); + var nameReference = new AssemblyNameReference("IPA.Injector", Program.Version); + var injectorPath = Path.Combine(_File.DirectoryName, "IPA.Injector.dll"); var injector = ModuleDefinition.ReadModule(injectorPath); for (int i = 0; i < _Module.AssemblyReferences.Count; i++) @@ -72,10 +72,11 @@ namespace IPA.Patcher _Module.AssemblyReferences.RemoveAt(i--); if (_Module.AssemblyReferences[i].Name == "IllusionPlugin") _Module.AssemblyReferences.RemoveAt(i--); + if (_Module.AssemblyReferences[i].Name == "IPA.Injector") + _Module.AssemblyReferences.RemoveAt(i--); } _Module.AssemblyReferences.Add(nameReference); - _Module.AssemblyReferences.Add(versionNameReference); int patched = 0; foreach(var type in FindEntryTypes()) @@ -100,7 +101,7 @@ namespace IPA.Patcher var targetMethod = targetType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); if (targetMethod != null) { - var methodReference = _Module.Import(injector.GetType("IllusionInjector.Injector").Methods.First(m => m.Name == "Inject")); + var methodReference = _Module.Import(injector.GetType("IPA.Injector.Injector").Methods.First(m => m.Name == "Inject")); targetMethod.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, methodReference)); return true; } diff --git a/IPA/Program.cs b/IPA/Program.cs index 257c3452..e697345d 100644 --- a/IPA/Program.cs +++ b/IPA/Program.cs @@ -19,7 +19,7 @@ namespace IPA { Unknown } - private static Version Version => Assembly.GetEntryAssembly().GetName().Version; + public static Version Version => Assembly.GetEntryAssembly().GetName().Version; public static ArgumentFlag ArgHelp = new ArgumentFlag("--help", "-h") { DocString = "prints this message" }; public static ArgumentFlag ArgWaitFor = new ArgumentFlag("--waitfor", "-w") { DocString = "waits for the specified PID to exit", ValueString = "PID" }; @@ -110,7 +110,7 @@ namespace IPA { #region Patch Version Check var patchedModule = PatchedModule.Load(context.EngineFile); - var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) > 0; + var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) >= 0; Console.WriteLine($"Current: {Version} Patched: {patchedModule.Data.Version}"); if (isCurrentNewer) { Console.ForegroundColor = ConsoleColor.White; diff --git a/IPA/Properties/AssemblyInfo.cs b/IPA/Properties/AssemblyInfo.cs index abe9ca50..4c0dbfcc 100644 --- a/IPA/Properties/AssemblyInfo.cs +++ b/IPA/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 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.8.9.*")] -[assembly: AssemblyFileVersion("3.8.9")] +[assembly: AssemblyVersion("3.9.0")] +[assembly: AssemblyFileVersion("3.9.0")] diff --git a/IllusionInjector/Updating/SelfPlugin.cs b/IllusionInjector/Updating/SelfPlugin.cs index 39863d70..b24f5642 100644 --- a/IllusionInjector/Updating/SelfPlugin.cs +++ b/IllusionInjector/Updating/SelfPlugin.cs @@ -12,7 +12,7 @@ namespace IllusionInjector.Updating internal class SelfPlugin : IBeatSaberPlugin { internal const string IPA_Name = "Beat Saber IPA"; - internal const string IPA_Version = "3.8.9"; + internal const string IPA_Version = "3.9.0"; public string Name => IPA_Name;