From 5eda901607ae52b17a60e6432cb268d5904f613d Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Sun, 5 Jan 2020 01:17:24 -0600 Subject: [PATCH] Switched entirely over to attribute based system --- IPA.Injector/Injector.cs | 682 ++++---- .../Loader/Composite/CompositeBSPlugin.cs | 135 +- IPA.Loader/Loader/Features/Feature.cs | 6 +- IPA.Loader/Loader/PluginExecutor.cs | 50 +- IPA.Loader/Loader/PluginInitInjector.cs | 17 +- IPA.Loader/Loader/PluginLoader.cs | 127 +- IPA.Loader/Loader/PluginManager.cs | 31 +- IPA.Loader/Logging/StandardLogger.cs | 1 + IPA.Loader/Updating/BeatMods/Updater.cs | 1546 ++++++++--------- 9 files changed, 1352 insertions(+), 1243 deletions(-) diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index 238f0b08..1d13abe4 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -1,342 +1,342 @@ -using IPA.Config; -using IPA.Injector.Backups; -using IPA.Loader; -using IPA.Logging; -using IPA.Utilities; -using Mono.Cecil; -using Mono.Cecil.Cil; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using UnityEngine; -using static IPA.Logging.Logger; -using MethodAttributes = Mono.Cecil.MethodAttributes; -#if NET3 -using Net3_Proxy; -using Path = Net3_Proxy.Path; -using File = Net3_Proxy.File; -using Directory = Net3_Proxy.Directory; -#endif - -namespace IPA.Injector -{ - /// - /// The entry point type for BSIPA's Doorstop injector. - /// - // ReSharper disable once UnusedMember.Global - internal static class Injector - { - private static Task pluginAsyncLoadTask; - private static Task permissionFixTask; - //private static string otherNewtonsoftJson = null; - - // ReSharper disable once UnusedParameter.Global - internal static void Main(string[] args) - { // entry point for doorstop - // At this point, literally nothing but mscorlib is loaded, - // and since this class doesn't have any static fields that - // aren't defined in mscorlib, we can control exactly what - // gets loaded. - - try - { - if (Environment.GetCommandLineArgs().Contains("--verbose")) - WinConsole.Initialize(); - - SetupLibraryLoading(); - - /*var otherNewtonsoft = Path.Combine( - Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(), - "Managed", - "Newtonsoft.Json.dll"); - if (File.Exists(otherNewtonsoft)) - { // this game ships its own Newtonsoft; force load ours and flag loading theirs - LibLoader.LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed")); - otherNewtonsoftJson = otherNewtonsoft; - }*/ - - EnsureDirectories(); - - // this is weird, but it prevents Mono from having issues loading the type. - // IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS - var unused = StandardLogger.PrintFilter; - #region // Above hack explaination - /* - * Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger - * is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to - * the above static property (or maybe any, but I don't really know) it behaves as expected and works fine. - */ - #endregion - - log.Debug("Initializing logger"); - - SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs()); - SelfConfig.Load(); - DisabledConfig.Load(); - - if (AntiPiracy.IsInvalid(Environment.CurrentDirectory)) - { - loader.Error("Invalid installation; please buy the game to run BSIPA."); - - return; - } - - CriticalSection.Configure(); - - loader.Debug("Prepping bootstrapper"); - - // updates backup - InstallBootstrapPatch(); - - GameVersionEarly.Load(); - - Updates.InstallPendingUpdates(); - - LibLoader.SetupAssemblyFilenames(true); - - pluginAsyncLoadTask = PluginLoader.LoadTask(); - permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory)); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - - private static void EnsureDirectories() - { - string path; - if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData"))) - Directory.CreateDirectory(path); - if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins"))) - Directory.CreateDirectory(path); - } - - private static void SetupLibraryLoading() - { - if (loadingDone) return; - loadingDone = true; - LibLoader.Configure(); - } - - private static void InstallHarmonyProtections() - { // proxy function to delay resolution - HarmonyProtectorProxy.ProtectNull(); - } - - private static void InstallBootstrapPatch() - { - var cAsmName = Assembly.GetExecutingAssembly().GetName(); - var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - var dataDir = new DirectoryInfo(managedPath).Parent.Name; - var gameName = dataDir.Substring(0, dataDir.Length - 5); - - loader.Debug("Finding backup"); - var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName); - var bkp = BackupManager.FindLatestBackup(backupPath); - if (bkp == null) - loader.Warn("No backup found! Was BSIPA installed using the installer?"); - - loader.Debug("Ensuring patch on UnityEngine.CoreModule exists"); - - #region Insert patch into UnityEngine.CoreModule.dll - - { - var unityPath = Path.Combine(managedPath, - "UnityEngine.CoreModule.dll"); - - // this is a critical section because if you exit in here, CoreModule can die - CriticalSection.EnterExecuteSection(); - - var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters - { - ReadWrite = false, - InMemory = true, - ReadingMode = ReadingMode.Immediate - }); - var unityModDef = unityAsmDef.MainModule; - - bool modified = false; - foreach (var asmref in unityModDef.AssemblyReferences) - { - if (asmref.Name == cAsmName.Name) - { - if (asmref.Version != cAsmName.Version) - { - asmref.Version = cAsmName.Version; - modified = true; - } - } - } - - var application = unityModDef.GetType("UnityEngine", "Application"); - - MethodDefinition cctor = null; - foreach (var m in application.Methods) - if (m.IsRuntimeSpecialName && m.Name == ".cctor") - cctor = m; - - var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); - - if (cctor == null) - { - cctor = new MethodDefinition(".cctor", - MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, - unityModDef.TypeSystem.Void); - application.Methods.Add(cctor); - modified = true; - - var ilp = cctor.Body.GetILProcessor(); - ilp.Emit(OpCodes.Call, cbs); - ilp.Emit(OpCodes.Ret); - } - else - { - var ilp = cctor.Body.GetILProcessor(); - for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) - { - var ins = cctor.Body.Instructions[i]; - switch (i) - { - case 0 when ins.OpCode != OpCodes.Call: - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - break; - - case 0: - { - var methodRef = ins.Operand as MethodReference; - if (methodRef?.FullName != cbs.FullName) - { - ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); - modified = true; - } - - break; - } - case 1 when ins.OpCode != OpCodes.Ret: - ilp.Replace(ins, ilp.Create(OpCodes.Ret)); - modified = true; - break; - } - } - } - - if (modified) - { - bkp?.Add(unityPath); - unityAsmDef.Write(unityPath); - } - - CriticalSection.ExitExecuteSection(); - } - - #endregion Insert patch into UnityEngine.CoreModule.dll - - loader.Debug("Ensuring game assemblies are virtualized"); - - #region Virtualize game assemblies - bool isFirst = true; - foreach(var name in SelfConfig.GameAssemblies_) - { - var ascPath = Path.Combine(managedPath, name); - - CriticalSection.EnterExecuteSection(); - - try - { - loader.Debug($"Virtualizing {name}"); - using var ascModule = VirtualizedModule.Load(ascPath); - ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath)); - } - catch (Exception e) - { - loader.Error($"Could not virtualize {ascPath}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - loader.Error(e); - } - - if (isFirst) - { - try - { - loader.Debug("Applying anti-yeet patch"); - - var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters - { - ReadWrite = false, - InMemory = true, - ReadingMode = ReadingMode.Immediate - }); - var ascModDef = ascAsmDef.MainModule; - - var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); - deleter.Methods.Clear(); // delete all methods - - ascAsmDef.Write(ascPath); - - isFirst = false; - } - catch (Exception e) - { - loader.Warn($"Could not apply anti-yeet patch to {ascPath}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - loader.Warn(e); - } - } - - CriticalSection.ExitExecuteSection(); - } - #endregion - } - - private static bool bootstrapped; - - private static void CreateBootstrapper() - { - if (bootstrapped) return; - bootstrapped = true; - - /*if (otherNewtonsoftJson != null) - Assembly.LoadFrom(otherNewtonsoftJson);*/ - -#if DEBUG - Config.Stores.GeneratedStore.DebugSaveAssembly("GeneratedAssembly.dll"); -#endif - - - Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type) - { - var level = UnityLogRedirector.LogTypeToLevel(type); - UnityLogProvider.UnityLogger.Log(level, $"{condition}"); - UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}"); - }; - - // need to reinit streams singe Unity seems to redirect stdout - StdoutInterceptor.RedirectConsole(); - - InstallHarmonyProtections(); - - var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); - bootstrapper.Destroyed += Bootstrapper_Destroyed; - } - - private static bool loadingDone; - - private static void Bootstrapper_Destroyed() - { - // wait for plugins to finish loading - pluginAsyncLoadTask.Wait(); - permissionFixTask.Wait(); - - BeatSaber.EnsureRuntimeGameVersion(); - - log.Debug("Plugins loaded"); - log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); - PluginComponent.Create(); - } - } +using IPA.Config; +using IPA.Injector.Backups; +using IPA.Loader; +using IPA.Logging; +using IPA.Utilities; +using Mono.Cecil; +using Mono.Cecil.Cil; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using UnityEngine; +using static IPA.Logging.Logger; +using MethodAttributes = Mono.Cecil.MethodAttributes; +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +using Directory = Net3_Proxy.Directory; +#endif + +namespace IPA.Injector +{ + /// + /// The entry point type for BSIPA's Doorstop injector. + /// + // ReSharper disable once UnusedMember.Global + internal static class Injector + { + private static Task pluginAsyncLoadTask; + private static Task permissionFixTask; + //private static string otherNewtonsoftJson = null; + + // ReSharper disable once UnusedParameter.Global + internal static void Main(string[] args) + { // entry point for doorstop + // At this point, literally nothing but mscorlib is loaded, + // and since this class doesn't have any static fields that + // aren't defined in mscorlib, we can control exactly what + // gets loaded. + + try + { + if (Environment.GetCommandLineArgs().Contains("--verbose")) + WinConsole.Initialize(); + + SetupLibraryLoading(); + + /*var otherNewtonsoft = Path.Combine( + Directory.EnumerateDirectories(Environment.CurrentDirectory, "*_Data").First(), + "Managed", + "Newtonsoft.Json.dll"); + if (File.Exists(otherNewtonsoft)) + { // this game ships its own Newtonsoft; force load ours and flag loading theirs + LibLoader.LoadLibrary(new AssemblyName("Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed")); + otherNewtonsoftJson = otherNewtonsoft; + }*/ + + EnsureDirectories(); + + // this is weird, but it prevents Mono from having issues loading the type. + // IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS + var unused = StandardLogger.PrintFilter; + #region // Above hack explaination + /* + * Due to an unknown bug in the version of Mono that Unity uses, if the first access to StandardLogger + * is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to + * the above static property (or maybe any, but I don't really know) it behaves as expected and works fine. + */ + #endregion + + log.Debug("Initializing logger"); + + SelfConfig.ReadCommandLine(Environment.GetCommandLineArgs()); + SelfConfig.Load(); + DisabledConfig.Load(); + + if (AntiPiracy.IsInvalid(Environment.CurrentDirectory)) + { + loader.Error("Invalid installation; please buy the game to run BSIPA."); + + return; + } + + CriticalSection.Configure(); + + loader.Debug("Prepping bootstrapper"); + + // updates backup + InstallBootstrapPatch(); + + GameVersionEarly.Load(); + + Updates.InstallPendingUpdates(); + + LibLoader.SetupAssemblyFilenames(true); + + pluginAsyncLoadTask = PluginLoader.LoadTask(); + permissionFixTask = PermissionFix.FixPermissions(new DirectoryInfo(Environment.CurrentDirectory)); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static void EnsureDirectories() + { + string path; + if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData"))) + Directory.CreateDirectory(path); + if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "Plugins"))) + Directory.CreateDirectory(path); + } + + private static void SetupLibraryLoading() + { + if (loadingDone) return; + loadingDone = true; + LibLoader.Configure(); + } + + private static void InstallHarmonyProtections() + { // proxy function to delay resolution + HarmonyProtectorProxy.ProtectNull(); + } + + private static void InstallBootstrapPatch() + { + var cAsmName = Assembly.GetExecutingAssembly().GetName(); + var managedPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + var dataDir = new DirectoryInfo(managedPath).Parent.Name; + var gameName = dataDir.Substring(0, dataDir.Length - 5); + + loader.Debug("Finding backup"); + var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", gameName); + var bkp = BackupManager.FindLatestBackup(backupPath); + if (bkp == null) + loader.Warn("No backup found! Was BSIPA installed using the installer?"); + + loader.Debug("Ensuring patch on UnityEngine.CoreModule exists"); + + #region Insert patch into UnityEngine.CoreModule.dll + + { + var unityPath = Path.Combine(managedPath, + "UnityEngine.CoreModule.dll"); + + // this is a critical section because if you exit in here, CoreModule can die + CriticalSection.EnterExecuteSection(); + + var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters + { + ReadWrite = false, + InMemory = true, + ReadingMode = ReadingMode.Immediate + }); + var unityModDef = unityAsmDef.MainModule; + + bool modified = false; + foreach (var asmref in unityModDef.AssemblyReferences) + { + if (asmref.Name == cAsmName.Name) + { + if (asmref.Version != cAsmName.Version) + { + asmref.Version = cAsmName.Version; + modified = true; + } + } + } + + var application = unityModDef.GetType("UnityEngine", "Application"); + + MethodDefinition cctor = null; + foreach (var m in application.Methods) + if (m.IsRuntimeSpecialName && m.Name == ".cctor") + cctor = m; + + var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method); + + if (cctor == null) + { + cctor = new MethodDefinition(".cctor", + MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, + unityModDef.TypeSystem.Void); + application.Methods.Add(cctor); + modified = true; + + var ilp = cctor.Body.GetILProcessor(); + ilp.Emit(OpCodes.Call, cbs); + ilp.Emit(OpCodes.Ret); + } + else + { + var ilp = cctor.Body.GetILProcessor(); + for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++) + { + var ins = cctor.Body.Instructions[i]; + switch (i) + { + case 0 when ins.OpCode != OpCodes.Call: + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; + break; + + case 0: + { + var methodRef = ins.Operand as MethodReference; + if (methodRef?.FullName != cbs.FullName) + { + ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs)); + modified = true; + } + + break; + } + case 1 when ins.OpCode != OpCodes.Ret: + ilp.Replace(ins, ilp.Create(OpCodes.Ret)); + modified = true; + break; + } + } + } + + if (modified) + { + bkp?.Add(unityPath); + unityAsmDef.Write(unityPath); + } + + CriticalSection.ExitExecuteSection(); + } + + #endregion Insert patch into UnityEngine.CoreModule.dll + + loader.Debug("Ensuring game assemblies are virtualized"); + + #region Virtualize game assemblies + bool isFirst = true; + foreach(var name in SelfConfig.GameAssemblies_) + { + var ascPath = Path.Combine(managedPath, name); + + CriticalSection.EnterExecuteSection(); + + try + { + loader.Debug($"Virtualizing {name}"); + using var ascModule = VirtualizedModule.Load(ascPath); + ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath)); + } + catch (Exception e) + { + loader.Error($"Could not virtualize {ascPath}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + loader.Error(e); + } + + if (isFirst) + { + try + { + loader.Debug("Applying anti-yeet patch"); + + var ascAsmDef = AssemblyDefinition.ReadAssembly(ascPath, new ReaderParameters + { + ReadWrite = false, + InMemory = true, + ReadingMode = ReadingMode.Immediate + }); + var ascModDef = ascAsmDef.MainModule; + + var deleter = ascModDef.GetType("IPAPluginsDirDeleter"); + deleter.Methods.Clear(); // delete all methods + + ascAsmDef.Write(ascPath); + + isFirst = false; + } + catch (Exception e) + { + loader.Warn($"Could not apply anti-yeet patch to {ascPath}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + loader.Warn(e); + } + } + + CriticalSection.ExitExecuteSection(); + } + #endregion + } + + private static bool bootstrapped; + + private static void CreateBootstrapper() + { + if (bootstrapped) return; + bootstrapped = true; + + /*if (otherNewtonsoftJson != null) + Assembly.LoadFrom(otherNewtonsoftJson);*/ + + + Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type) + { + var level = UnityLogRedirector.LogTypeToLevel(type); + UnityLogProvider.UnityLogger.Log(level, $"{condition}"); + UnityLogProvider.UnityLogger.Log(level, $"{stackTrace}"); + }; + + // need to reinit streams singe Unity seems to redirect stdout + StdoutInterceptor.RedirectConsole(); + + InstallHarmonyProtections(); + + var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); + bootstrapper.Destroyed += Bootstrapper_Destroyed; + } + + private static bool loadingDone; + + private static void Bootstrapper_Destroyed() + { + // wait for plugins to finish loading + pluginAsyncLoadTask.Wait(); + permissionFixTask.Wait(); + + BeatSaber.EnsureRuntimeGameVersion(); + + log.Debug("Plugins loaded"); + log.Debug(string.Join(", ", PluginLoader.PluginsMetadata.StrJP())); + PluginComponent.Create(); + +#if DEBUG + Config.Stores.GeneratedStore.DebugSaveAssembly("GeneratedAssembly.dll"); +#endif + } + } } \ No newline at end of file diff --git a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs index 40399252..386443d9 100644 --- a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs +++ b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs @@ -1,68 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using UnityEngine.SceneManagement; -using Logger = IPA.Logging.Logger; - -namespace IPA.Loader.Composite -{ - internal class CompositeBSPlugin - { - private readonly IEnumerable plugins; - - private delegate void CompositeCall(PluginLoader.PluginInfo plugin); - - public CompositeBSPlugin(IEnumerable plugins) - { - this.plugins = plugins; - } - private void Invoke(CompositeCall callback, [CallerMemberName] string method = "") - { - foreach (var plugin in plugins) - { - try - { - if (plugin.Plugin != null) - callback(plugin); - } - catch (Exception ex) - { - Logger.log.Error($"{plugin.Metadata.Name} {method}: {ex}"); - } - } - } - - public void OnEnable() - => Invoke(plugin => plugin.Plugin.OnEnable()); - - public void OnApplicationQuit() - => Invoke(plugin => plugin.Plugin.OnApplicationQuit()); - - public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) - => Invoke(plugin => plugin.Plugin.OnSceneLoaded(scene, sceneMode)); - - public void OnSceneUnloaded(Scene scene) - => Invoke(plugin => plugin.Plugin.OnSceneUnloaded(scene)); - - public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) - => Invoke(plugin => plugin.Plugin.OnActiveSceneChanged(prevScene, nextScene)); - - public void OnUpdate() - => Invoke(plugin => { - if (plugin.Plugin is IEnhancedPlugin saberPlugin) - saberPlugin.OnUpdate(); - }); - - public void OnFixedUpdate() - => Invoke(plugin => { - if (plugin.Plugin is IEnhancedPlugin saberPlugin) - saberPlugin.OnFixedUpdate(); - }); - - public void OnLateUpdate() - => Invoke(plugin => { - if (plugin.Plugin is IEnhancedPlugin saberPlugin) - saberPlugin.OnLateUpdate(); - }); - } +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using UnityEngine.SceneManagement; +using Logger = IPA.Logging.Logger; + +namespace IPA.Loader.Composite +{ + internal class CompositeBSPlugin + { + private readonly IEnumerable plugins; + + private delegate void CompositeCall(PluginExecutor plugin); + + public CompositeBSPlugin(IEnumerable plugins) + { + this.plugins = plugins; + } + private void Invoke(CompositeCall callback, [CallerMemberName] string method = "") + { + foreach (var plugin in plugins) + { + try + { + if (plugin != null) + callback(plugin); + } + catch (Exception ex) + { + Logger.log.Error($"{plugin.Metadata.Name} {method}: {ex}"); + } + } + } + + public void OnEnable() + => Invoke(plugin => plugin.Enable()); + + public void OnApplicationQuit() // do something useful with the Task that Disable gives us + => Invoke(plugin => plugin.Disable()); + + public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) + { }//=> Invoke(plugin => plugin.Plugin.OnSceneLoaded(scene, sceneMode)); + + public void OnSceneUnloaded(Scene scene) + { }//=> Invoke(plugin => plugin.Plugin.OnSceneUnloaded(scene)); + + public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) + { }//=> Invoke(plugin => plugin.Plugin.OnActiveSceneChanged(prevScene, nextScene)); + + public void OnUpdate() + { }/*=> Invoke(plugin => + { + if (plugin.Plugin is IEnhancedPlugin saberPlugin) + saberPlugin.OnUpdate(); + });*/ + + public void OnFixedUpdate() + { }/*=> Invoke(plugin => { + if (plugin.Plugin is IEnhancedPlugin saberPlugin) + saberPlugin.OnFixedUpdate(); + });*/ + + public void OnLateUpdate() + { }/*=> Invoke(plugin => { + if (plugin.Plugin is IEnhancedPlugin saberPlugin) + saberPlugin.OnLateUpdate(); + });*/ + } } \ No newline at end of file diff --git a/IPA.Loader/Loader/Features/Feature.cs b/IPA.Loader/Loader/Features/Feature.cs index 039d3d3b..ba5c4ecb 100644 --- a/IPA.Loader/Loader/Features/Feature.cs +++ b/IPA.Loader/Loader/Features/Feature.cs @@ -61,20 +61,20 @@ namespace IPA.Loader.Features /// /// the plugin to be initialized /// whether or not to call the Init method - public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; + public virtual bool BeforeInit(PluginMetadata plugin) => true; /// /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. /// /// the plugin that was just initialized /// the instance of the plugin being initialized - public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin); + public virtual void AfterInit(PluginMetadata plugin, object pluginInstance) => AfterInit(plugin); /// /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. /// /// the plugin that was just initialized - public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } + public virtual void AfterInit(PluginMetadata plugin) { } /// /// Ensures a plugin's assembly is loaded. Do not use unless you need to. diff --git a/IPA.Loader/Loader/PluginExecutor.cs b/IPA.Loader/Loader/PluginExecutor.cs index f7a0bfc4..e5196679 100644 --- a/IPA.Loader/Loader/PluginExecutor.cs +++ b/IPA.Loader/Loader/PluginExecutor.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Linq.Expressions; #if NET4 using Task = System.Threading.Tasks.Task; +using TaskEx = System.Threading.Tasks.Task; #endif #if NET3 using Net3_Proxy; @@ -20,14 +21,21 @@ namespace IPA.Loader internal class PluginExecutor { public PluginMetadata Metadata { get; } - public PluginExecutor(PluginMetadata meta) + public PluginExecutor(PluginMetadata meta, bool isSelf) { Metadata = meta; - PrepareDelegates(); + if (isSelf) + { + CreatePlugin = m => null; + LifecycleEnable = o => { }; + LifecycleDisable = o => TaskEx.CompletedTask; + } + else + PrepareDelegates(); } - private object pluginObject = null; + public object Instance { get; private set; } = null; private Func CreatePlugin { get; set; } private Action LifecycleEnable { get; set; } // disable may be async (#24) @@ -35,12 +43,12 @@ namespace IPA.Loader public void Create() { - if (pluginObject != null) return; - pluginObject = CreatePlugin(Metadata); + if (Instance != null) return; + Instance = CreatePlugin(Metadata); } - public void Enable() => LifecycleEnable(pluginObject); - public Task Disable() => LifecycleDisable(pluginObject); + public void Enable() => LifecycleEnable(Instance); + public Task Disable() => LifecycleDisable(Instance); private void PrepareDelegates() @@ -85,21 +93,23 @@ namespace IPA.Loader } // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3? - var metaParam = Expression.Parameter(typeof(PluginMetadata)); - var objVar = Expression.Variable(type); + var metaParam = Expression.Parameter(typeof(PluginMetadata), "meta"); + var objVar = Expression.Variable(type, "objVar"); + var persistVar = Expression.Variable(typeof(object), "persistVar"); var createExpr = Expression.Lambda>( - Expression.Block( + Expression.Block(new[] { objVar, persistVar }, initMethods - .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es))) + .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, persistVar, es => Expression.Call(objVar, m, es))) .Prepend(Expression.Assign(objVar, usingDefaultCtor ? Expression.New(ctor) - : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es)))) + : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, persistVar, es => Expression.New(ctor, es)))) .Append(Expression.Convert(objVar, typeof(object)))), metaParam); // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler return createExpr.Compile(); } + // TODO: make enable and disable able to take a bool indicating which it is private static Action MakeLifecycleEnableFunc(Type type, string name) { var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) @@ -121,10 +131,10 @@ namespace IPA.Loader Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); } - var objParam = Expression.Parameter(typeof(object)); - var instVar = Expression.Variable(type); + var objParam = Expression.Parameter(typeof(object), "obj"); + var instVar = Expression.Variable(type, "inst"); var createExpr = Expression.Lambda>( - Expression.Block( + Expression.Block(new[] { instVar }, enableMethods .Select(m => Expression.Call(instVar, m)) .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type)))), @@ -164,14 +174,14 @@ namespace IPA.Loader nonTaskMethods.Add(m); } - Expression> completedTaskDel = () => Task.CompletedTask; + Expression> completedTaskDel = () => TaskEx.CompletedTask; var getCompletedTask = completedTaskDel.Body; - var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static); + var taskWhenAll = typeof(TaskEx).GetMethod(nameof(TaskEx.WhenAll), new[] { typeof(Task[]) }); - var objParam = Expression.Parameter(typeof(object)); - var instVar = Expression.Variable(type); + var objParam = Expression.Parameter(typeof(object), "obj"); + var instVar = Expression.Variable(type, "inst"); var createExpr = Expression.Lambda>( - Expression.Block( + Expression.Block(new[] { instVar }, nonTaskMethods .Select(m => Expression.Call(instVar, m)) .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type))) diff --git a/IPA.Loader/Loader/PluginInitInjector.cs b/IPA.Loader/Loader/PluginInitInjector.cs index c88ca9f1..89556778 100644 --- a/IPA.Loader/Loader/PluginInitInjector.cs +++ b/IPA.Loader/Loader/PluginInitInjector.cs @@ -96,22 +96,27 @@ namespace IPA.Loader } private static readonly MethodInfo InjectMethod = typeof(PluginInitInjector).GetMethod(nameof(Inject), BindingFlags.NonPublic | BindingFlags.Static); - internal static Expression InjectedCallExpr(ParameterInfo[] initParams, Expression meta, Func, Expression> exprGen) + internal static Expression InjectedCallExpr(ParameterInfo[] initParams, Expression meta, ParameterExpression persistVar, Func, Expression> exprGen) { - var arr = Expression.Variable(typeof(object[])); - return Expression.Block( - Expression.Assign(arr, Expression.Call(InjectMethod, Expression.Constant(initParams), meta)), + var arr = Expression.Variable(typeof(object[]), "initArr"); + return Expression.Block(new[] { arr }, + Expression.Assign(arr, Expression.Call(InjectMethod, Expression.Constant(initParams), meta, persistVar)), exprGen(initParams .Select(p => p.ParameterType) .Select((t, i) => Expression.Convert( Expression.ArrayIndex(arr, Expression.Constant(i)), t)))); } - internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta) + internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta, ref object persist) { var initArgs = new List(); - var previousValues = new Dictionary(injectors.Count); + var previousValues = persist as Dictionary; + if (previousValues == null) + { + previousValues = new Dictionary(injectors.Count); + persist = previousValues; + } foreach (var param in initParams) { diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index ef7893e2..7cfa36c0 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -622,15 +622,118 @@ namespace IPA.Loader meta.Assembly = Assembly.LoadFrom(meta.File.FullName); } - internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) + internal static PluginExecutor InitPlugin(PluginMetadata meta, IEnumerable alreadyLoaded) { - if (meta.IsAttributePlugin) + if (meta.Manifest.GameVersion != BeatSaber.GameVersion) + Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); + + if (!meta.IsAttributePlugin) + { + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Non-attribute plugins are currently not supported" }); + return null; + } + + if (meta.IsSelf) + return new PluginExecutor(meta, true); + + foreach (var dep in meta.Dependencies) + { + if (alreadyLoaded.Contains(dep)) continue; + + // otherwise... + + if (ignoredPlugins.TryGetValue(dep, out var reason)) + { // was added to the ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}", + RelatedTo = dep + }); + } + else + { // was not added to ignore list + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency) + { + ReasonText = $"Dependency was not already loaded at load time, but was also not ignored", + RelatedTo = dep + }); + } + + return null; + } + + Load(meta); + + foreach (var feature in meta.Features) + { + if (!feature.BeforeLoad(meta)) + { + Logger.loader.Warn( + $"Feature {feature?.GetType()} denied plugin {meta.Name} from loading! {feature?.InvalidMessage}"); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}" + }); + return null; + } + } + + PluginExecutor exec; + try { - ignoredPlugins.Add(meta, new IgnoreReason(Reason.Unsupported) { ReasonText = "Attribute plugins are currently not supported" }); + exec = new PluginExecutor(meta, false); + } + catch (Exception e) + { + Logger.loader.Error($"Error creating executor for {meta.Name}"); + Logger.loader.Error(e); + return null; + } + + foreach (var feature in meta.Features) + { + if (!feature.BeforeInit(meta)) + { + Logger.loader.Warn( + $"Feature {feature?.GetType()} denied plugin {meta.Name} from initializing! {feature?.InvalidMessage}"); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) + { + ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}" + }); + return null; + } + } + + try + { + exec.Create(); + } + catch (Exception e) + { + Logger.loader.Error($"Could not init plugin {meta.Name}"); + Logger.loader.Error(e); + ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error) + { + ReasonText = "Error ocurred while initializing", + Error = e + }); return null; } - if (meta.PluginType == null) + foreach (var feature in meta.Features) + try + { + feature.AfterInit(meta, exec.Instance); + } + catch (Exception e) + { + Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}"); + } + + return exec; + + #region Interface plugin support + /*if (meta.IsSelf) return new PluginInfo() { Metadata = meta, @@ -639,9 +742,6 @@ namespace IPA.Loader var info = new PluginInfo(); - if (meta.Manifest.GameVersion != BeatSaber.GameVersion) - Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly."); - try { foreach (var dep in meta.Dependencies) @@ -738,22 +838,23 @@ namespace IPA.Loader return null; } - return info; + return info;*/ + #endregion } - internal static List LoadPlugins() + internal static List LoadPlugins() { InitFeatures(); DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily - var list = new List(); + var list = new List(); var loaded = new HashSet(); foreach (var meta in PluginsMetadata) { - var info = InitPlugin(meta, loaded); - if (info != null) + var exec = InitPlugin(meta, loaded); + if (exec != null) { - list.Add(info); + list.Add(exec); loaded.Add(meta); } } diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 34d52def..8dc54a24 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -33,19 +33,16 @@ namespace IPA.Loader /// /// An of new Beat Saber plugins /// - internal static IEnumerable BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin); - private static List _bsPlugins; - internal static IEnumerable BSMetas => _bsPlugins; + private static List _bsPlugins; + internal static IEnumerable BSMetas => _bsPlugins; /// /// Gets info about the plugin with the specified name. /// /// the name of the plugin to get (must be an exact match) /// the plugin info for the requested plugin or null - public static PluginInfo GetPlugin(string name) - { - return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); - } + public static PluginMetadata GetPlugin(string name) + => BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Name == name); /// /// Gets info about the plugin with the specified ModSaber name. @@ -53,17 +50,15 @@ namespace IPA.Loader /// the ModSaber name of the plugin to get (must be an exact match) /// the plugin info for the requested plugin or null [Obsolete("Old name. Use GetPluginFromId instead.")] - public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name); + public static PluginMetadata GetPluginFromModSaberName(string name) => GetPluginFromId(name); /// /// Gets info about the plugin with the specified ID. /// /// the ID name of the plugin to get (must be an exact match) /// the plugin info for the requested plugin or null - public static PluginInfo GetPluginFromId(string name) - { - return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); - } + public static PluginMetadata GetPluginFromId(string name) + => BSMetas.Select(p => p.Metadata).FirstOrDefault(p => p.Id == name); /// /// Gets a disabled plugin's metadata by its name. @@ -81,6 +76,8 @@ namespace IPA.Loader public static PluginMetadata GetDisabledPluginFromId(string name) => DisabledPlugins.FirstOrDefault(p => p.Id == name); + // TODO: rewrite below + /* /// /// Disables a plugin, and all dependents. /// @@ -220,7 +217,7 @@ namespace IPA.Loader /// the ID, or name if the ID is null, of the plugin to enable /// whether a restart is needed to activate public static bool EnablePlugin(string pluginId) => - EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId)); + EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId));*/ /// /// Checks if a given plugin is disabled. @@ -269,8 +266,9 @@ namespace IPA.Loader /// Gets a list of all BSIPA plugins. /// /// a collection of all enabled plugins as s - public static IEnumerable AllPlugins => BSMetas; + public static IEnumerable AllPlugins => BSMetas.Select(p => p.Metadata); + /* /// /// Converts a plugin's metadata to a . /// @@ -281,8 +279,9 @@ namespace IPA.Loader if (IsDisabled(meta)) return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); else - return AllPlugins.FirstOrDefault(p => p.Metadata == meta); + return AllPlugins.FirstOrDefault(p => p == meta); } + */ /// /// An of old IPA plugins. @@ -301,7 +300,7 @@ namespace IPA.Loader // Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL, // so we need to resort to P/Invoke string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath); - _bsPlugins = new List(); + _bsPlugins = new List(); _ipaPlugins = new List(); if (!Directory.Exists(pluginDirectory)) return; diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs index b9c7c880..502fffd5 100644 --- a/IPA.Loader/Logging/StandardLogger.cs +++ b/IPA.Loader/Logging/StandardLogger.cs @@ -193,6 +193,7 @@ namespace IPA.Logging if (message == null) throw new ArgumentNullException(nameof(message)); + // FIXME: trace doesn't seem to ever actually appear if (!showTrace && level == Level.Trace) return; // make sure that the queue isn't being cleared diff --git a/IPA.Loader/Updating/BeatMods/Updater.cs b/IPA.Loader/Updating/BeatMods/Updater.cs index d09f3082..504f7b06 100644 --- a/IPA.Loader/Updating/BeatMods/Updater.cs +++ b/IPA.Loader/Updating/BeatMods/Updater.cs @@ -1,777 +1,769 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Runtime.Serialization; -using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; -using Ionic.Zip; -using IPA.Config; -using IPA.Loader; -using IPA.Loader.Features; -using IPA.Utilities; -using Newtonsoft.Json; -using SemVer; -using UnityEngine; -using UnityEngine.Networking; -using static IPA.Loader.PluginManager; -using Logger = IPA.Logging.Logger; -using Version = SemVer.Version; - -namespace IPA.Updating.BeatMods -{ - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - internal partial class Updater : MonoBehaviour - { - internal const string SpecialDeletionsFile = "$$delete"; - } - -#if BeatSaber - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - internal partial class Updater : MonoBehaviour - { - public static Updater Instance; - - internal static bool ModListPresent = false; - - public void Awake() - { - try - { - if (Instance != null) - Destroy(this); - else - { - Instance = this; - DontDestroyOnLoad(this); - if (!ModListPresent && SelfConfig.Updates_.AutoCheckUpdates_) - CheckForUpdates(); - } - } - catch (Exception e) - { - Logger.updater.Error(e); - } - } - - internal delegate void CheckUpdatesComplete(List toUpdate); - - public void CheckForUpdates(CheckUpdatesComplete onComplete = null) => StartCoroutine(CheckForUpdatesCoroutine(onComplete)); - - internal class DependencyObject - { - public string Name { get; set; } - public Version Version { get; set; } - public Version ResolvedVersion { get; set; } - public Range Requirement { get; set; } - public Range Conflicts { get; set; } // a range of versions that are not allowed to be downloaded - public bool Resolved { get; set; } - public bool Has { get; set; } - public HashSet Consumers { get; set; } = new HashSet(); - - public bool MetaRequestFailed { get; set; } - - public PluginLoader.PluginInfo LocalPluginMeta { get; set; } - - public bool IsLegacy { get; set; } = false; - - public override string ToString() - { - return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement} ! {Conflicts}) {(Has ? " Already have" : "")}"; - } - } - - public static void ResetRequestCache() - { - requestCache.Clear(); - modCache.Clear(); - modVersionsCache.Clear(); - } - - private static readonly Dictionary requestCache = new Dictionary(); - private static IEnumerator GetBeatModsEndpoint(string url, Ref result) - { - if (requestCache.TryGetValue(url, out string value)) - { - result.Value = value; - } - else - { - using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + url)) - { - yield return request.SendWebRequest(); - - if (request.isNetworkError) - { - result.Error = new NetworkException($"Network error while trying to download: {request.error}"); - yield break; - } - if (request.isHttpError) - { - if (request.responseCode == 404) - { - result.Error = new NetworkException("Not found"); - yield break; - } - - result.Error = new NetworkException($"Server returned error {request.error} while getting data"); - yield break; - } - - result.Value = request.downloadHandler.text; - - requestCache[url] = result.Value; - } - } - } - - private static readonly Dictionary modCache = new Dictionary(); - internal static IEnumerator GetModInfo(string modName, string ver, Ref result) - { - var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeDataString(modName), Uri.EscapeDataString(ver)); - - if (modCache.TryGetValue(uri, out ApiEndpoint.Mod value)) - { - result.Value = value; - } - else - { - Ref reqResult = new Ref(""); - - yield return GetBeatModsEndpoint(uri, reqResult); - - try - { - result.Value = JsonConvert.DeserializeObject>(reqResult.Value).First(); - - modCache[uri] = result.Value; - } - catch (Exception e) - { - result.Error = new Exception("Error decoding response", e); - } - } - } - - private static readonly Dictionary> modVersionsCache = new Dictionary>(); - internal static IEnumerator GetModVersionsMatching(string modName, Range range, Ref> result) - { - var uri = string.Format(ApiEndpoint.GetModsByName, Uri.EscapeDataString(modName)); - - if (modVersionsCache.TryGetValue(uri, out List value)) - { - result.Value = value; - } - else - { - Ref reqResult = new Ref(""); - - yield return GetBeatModsEndpoint(uri, reqResult); - - try - { - result.Value = JsonConvert.DeserializeObject>(reqResult.Value) - .Where(m => range.IsSatisfied(m.Version)).ToList(); - - modVersionsCache[uri] = result.Value; - } - catch (Exception e) - { - result.Error = new Exception("Error decoding response", e); - } - } - } - - internal IEnumerator CheckForUpdatesCoroutine(CheckUpdatesComplete onComplete) - { - var depList = new Ref>(new List()); - - foreach (var plugin in BSMetas) - { // initialize with data to resolve (1.1) - if (plugin.Metadata.Id != null) - { // updatable - var msinfo = plugin.Metadata; - var dep = new DependencyObject - { - Name = msinfo.Id, - Version = msinfo.Version, - Requirement = new Range($">={msinfo.Version}"), - LocalPluginMeta = plugin - }; - - if (msinfo.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) - { // disable updating, by only matching self, so that dependencies can still be resolved - dep.Requirement = new Range(msinfo.Version.ToString()); - } - - depList.Value.Add(dep); - } - } - - foreach (var meta in PluginLoader.ignoredPlugins.Keys) - { // update ignored - if (meta.Id != null) - { // updatable - var dep = new DependencyObject - { - Name = meta.Id, - Version = meta.Version, - Requirement = new Range($">={meta.Version}"), - LocalPluginMeta = new PluginLoader.PluginInfo - { - Metadata = meta, - Plugin = null - } - }; - - if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) - { // disable updating, by only matching self - dep.Requirement = new Range(meta.Version.ToString()); - } - - depList.Value.Add(dep); - } - } - - foreach (var meta in DisabledPlugins) - { // update ignored - if (meta.Id != null) - { // updatable - var dep = new DependencyObject - { - Name = meta.Id, - Version = meta.Version, - Requirement = new Range($">={meta.Version}"), - LocalPluginMeta = new PluginLoader.PluginInfo - { - Metadata = meta, - Plugin = null - } - }; - - if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) - { // disable updating, by only matching self - dep.Requirement = new Range(meta.Version.ToString()); - } - - depList.Value.Add(dep); - } - } - -#pragma warning disable CS0618 // Type or member is obsolete - foreach (var plug in Plugins) - { // throw these in the updater on the off chance that they are set up properly - try - { - var dep = new DependencyObject - { - Name = plug.Name, - Version = new Version(plug.Version), - Requirement = new Range($">={plug.Version}"), - IsLegacy = true, - LocalPluginMeta = null - }; - - depList.Value.Add(dep); - } - catch (Exception e) - { - Logger.updater.Warn($"Error trying to add legacy plugin {plug.Name} to updater"); - Logger.updater.Warn(e); - } - } -#pragma warning restore CS0618 // Type or member is obsolete - - foreach (var dep in depList.Value) - Logger.updater.Debug($"Phantom Dependency: {dep}"); - - yield return ResolveDependencyRanges(depList); - - foreach (var dep in depList.Value) - Logger.updater.Debug($"Dependency: {dep}"); - - yield return ResolveDependencyPresence(depList); - - foreach (var dep in depList.Value) - Logger.updater.Debug($"Dependency: {dep}"); - - CheckDependencies(depList); - - onComplete?.Invoke(depList); - - if (!ModListPresent && SelfConfig.Updates_.AutoUpdate_) - StartDownload(depList.Value); - } - - internal IEnumerator ResolveDependencyRanges(Ref> list) - { - for (int i = 0; i < list.Value.Count; i++) - { // Grab dependencies (1.2) - var dep = list.Value[i]; - - var mod = new Ref(null); - - yield return GetModInfo(dep.Name, "", mod); - - try { mod.Verify(); } - catch (Exception e) - { - Logger.updater.Error($"Error getting info for {dep.Name}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logger.updater.Error(e); - dep.MetaRequestFailed = true; - continue; - } - - list.Value.AddRange(mod.Value.Dependencies.Select(m => new DependencyObject - { - Name = m.Name, - Requirement = new Range($"^{m.Version}"), - Consumers = new HashSet { dep.Name } - })); - // currently no conflicts exist in BeatMods - //list.Value.AddRange(mod.Value.Links.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = d.VersionRange, Consumers = new HashSet { dep.Name } })); - //list.Value.AddRange(mod.Value.Links.Conflicts.Select(d => new DependencyObject { Name = d.Name, Conflicts = d.VersionRange, Consumers = new HashSet { dep.Name } })); - } - - var depNames = new HashSet(); - var final = new List(); - - foreach (var dep in list.Value) - { // agregate ranges and the like (1.3) - if (!depNames.Contains(dep.Name)) - { // should add it - depNames.Add(dep.Name); - final.Add(dep); - } - else - { - var toMod = final.First(d => d.Name == dep.Name); - - if (dep.Requirement != null) - { - toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement); - foreach (var consume in dep.Consumers) - toMod.Consumers.Add(consume); - } - if (dep.Conflicts != null) - { - toMod.Conflicts = toMod.Conflicts == null - ? dep.Conflicts - : new Range($"{toMod.Conflicts} || {dep.Conflicts}"); - } - } - } - - list.Value = final; - } - - internal IEnumerator ResolveDependencyPresence(Ref> list) - { - foreach(var dep in list.Value) - { - dep.Has = dep.Version != null; // dep.Version is only not null if its already installed - - if (dep.MetaRequestFailed) - { - Logger.updater.Warn($"{dep.Name} info request failed, not trying again"); - continue; - } - - var modsMatching = new Ref>(null); - yield return GetModVersionsMatching(dep.Name, dep.Requirement, modsMatching); - try { modsMatching.Verify(); } - catch (Exception e) - { - Logger.updater.Error($"Error getting mod list for {dep.Name}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logger.updater.Error(e); - dep.MetaRequestFailed = true; - continue; - } - - var ver = modsMatching.Value - .NonNull() // entry is not null - .Where(versionCheck => versionCheck.GameVersion == BeatSaber.GameVersion) // game version matches - .Where(approvalCheck => approvalCheck.Status == ApiEndpoint.Mod.ApprovedStatus) // version approved - // TODO: fix; it seems wrong somehow - .Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version)) // not a conflicting version - .Select(mod => mod.Version).Max(); // (2.1) get the max version - dep.Resolved = ver != null; - if (dep.Resolved) dep.ResolvedVersion = ver; // (2.2) - dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion; - } - } - - internal void CheckDependencies(Ref> list) - { - var toDl = new List(); - - foreach (var dep in list.Value) - { // figure out which ones need to be downloaded (3.1) - if (dep.Resolved) - { - Logger.updater.Debug($"Resolved: {dep}"); - if (!dep.Has) - { - Logger.updater.Debug($"To Download: {dep}"); - toDl.Add(dep); - } - } - else if (!dep.Has) - { - if (dep.Version != null && dep.Requirement.IsSatisfied(dep.Version)) - Logger.updater.Notice($"Mod {dep.Name} running a newer version than is on BeatMods ({dep.Version})"); - else - Logger.updater.Warn($"Could not resolve dependency {dep}"); - } - } - - Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}"); - - list.Value = toDl; - } - - internal delegate void DownloadStart(DependencyObject obj); - internal delegate void DownloadProgress(DependencyObject obj, long totalBytes, long currentBytes, double progress); - internal delegate void DownloadFailed(DependencyObject obj, string error); - internal delegate void DownloadFinish(DependencyObject obj); - /// - /// This will still be called even if there was an error. Called after all three download/install attempts, or after a successful installation. - /// ALWAYS called. - /// - /// - /// - internal delegate void InstallFinish(DependencyObject obj, bool didError); - /// - /// This can be called multiple times - /// - /// - /// - internal delegate void InstallFailed(DependencyObject obj, Exception error); - - internal void StartDownload(IEnumerable download, DownloadStart downloadStart = null, - DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null, - InstallFailed installFail = null, InstallFinish installFinish = null) - { - foreach (var item in download) - StartCoroutine(UpdateModCoroutine(item, downloadStart, downloadProgress, downloadFail, downloadFinish, installFail, installFinish)); - } - - private static IEnumerator UpdateModCoroutine(DependencyObject item, DownloadStart downloadStart, - DownloadProgress progress, DownloadFailed dlFail, DownloadFinish finish, - InstallFailed installFail, InstallFinish installFinish) - { // (3.2) - Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}"); - - var mod = new Ref(null); - yield return GetModInfo(item.Name, item.ResolvedVersion.ToString(), mod); - try { mod.Verify(); } - catch (Exception e) - { - Logger.updater.Error($"Error occurred while trying to get information for {item}"); - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logger.updater.Error(e); - yield break; - } - - var releaseName = BeatSaber.ReleaseType == BeatSaber.Release.Steam - ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus; - var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName); - - string url = ApiEndpoint.BeatModBase + platformFile.Path; - - Logger.updater.Debug($"URL = {url}"); - - const int maxTries = 3; - int tries = maxTries; - while (tries > 0) - { - if (tries-- != maxTries) - Logger.updater.Debug("Re-trying download..."); - - using (var stream = new MemoryStream()) - using (var request = UnityWebRequest.Get(url)) - using (var taskTokenSource = new CancellationTokenSource()) - { - var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress?.Invoke(item, i1, i2, d)); - request.downloadHandler = dlh; - - downloadStart?.Invoke(item); - - Logger.updater.Debug("Sending request"); - //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL"); - yield return request.SendWebRequest(); - Logger.updater.Debug("Download finished"); - - if (request.isNetworkError) - { - Logger.updater.Error("Network error while trying to update mod"); - Logger.updater.Error(request.error); - dlFail?.Invoke(item, request.error); - taskTokenSource.Cancel(); - continue; - } - if (request.isHttpError) - { - Logger.updater.Error("Server returned an error code while trying to update mod"); - Logger.updater.Error(request.error); - dlFail?.Invoke(item, request.error); - taskTokenSource.Cancel(); - continue; - } - - finish?.Invoke(item); - - stream.Seek(0, SeekOrigin.Begin); // reset to beginning - - var downloadTask = Task.Run(() => - { // use slightly more multi threaded approach than co-routines - // ReSharper disable once AccessToDisposedClosure - ExtractPluginAsync(stream, item, platformFile); - }, taskTokenSource.Token); - - while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted)) - yield return null; // pause co-routine until task is done - - if (downloadTask.IsFaulted) - { - if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException)) - { // any exception is an intercept exception - Logger.updater.Error($"BeatMods did not return expected data for {item.Name}"); - } - else - Logger.updater.Error($"Error downloading mod {item.Name}"); - - if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) - Logger.updater.Error(downloadTask.Exception); - - installFail?.Invoke(item, downloadTask.Exception); - continue; - } - - break; - } - } - - if (tries == 0) - { - Logger.updater.Warn($"Plugin download failed {maxTries} times, not re-trying"); - - installFinish?.Invoke(item, true); - } - else - { - Logger.updater.Debug("Download complete"); - installFinish?.Invoke(item, false); - } - } - - internal class StreamDownloadHandler : DownloadHandlerScript - { - internal int length; - internal int cLen; - internal Action progress; - public MemoryStream Stream { get; set; } - - public StreamDownloadHandler(MemoryStream stream, Action progress = null) - { - Stream = stream; - this.progress = progress; - } - - protected override void ReceiveContentLength(int contentLength) - { - Stream.Capacity = length = contentLength; - cLen = 0; - Logger.updater.Debug($"Got content length: {contentLength}"); - } - - protected override void CompleteContent() - { - Logger.updater.Debug("Download complete"); - } - - protected override bool ReceiveData(byte[] rData, int dataLength) - { - if (rData == null || rData.Length < 1) - { - Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer"); - return false; - } - - cLen += dataLength; - - Stream.Write(rData, 0, dataLength); - - progress?.Invoke(length, cLen, ((double)cLen) / length); - - return true; - } - - protected override byte[] GetData() { return null; } - - protected override float GetProgress() - { - return 0f; - } - - public override string ToString() - { - return $"{base.ToString()} ({Stream})"; - } - } - - private static void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo) - { // (3.3) - Logger.updater.Debug($"Extracting ZIP file for {item.Name}"); - - /*var data = stream.GetBuffer(); - SHA1 sha = new SHA1CryptoServiceProvider(); - var hash = sha.ComputeHash(data); - if (!Utils.UnsafeCompare(hash, fileInfo.Hash)) - throw new Exception("The hash for the file doesn't match what is defined");*/ - - var targetDir = Path.Combine(BeatSaber.InstallPath, "IPA", Path.GetRandomFileName() + "_Pending"); - Directory.CreateDirectory(targetDir); - - var eventualOutput = Path.Combine(BeatSaber.InstallPath, "IPA", "Pending"); - if (!Directory.Exists(eventualOutput)) - Directory.CreateDirectory(eventualOutput); - - try - { - bool shouldDeleteOldFile = !(item.LocalPluginMeta?.Metadata.IsSelf).Unwrap(); - - using (var zipFile = ZipFile.Read(stream)) - { - Logger.updater.Debug("Streams opened"); - foreach (var entry in zipFile) - { - if (entry.IsDirectory) - { - Logger.updater.Debug($"Creating directory {entry.FileName}"); - Directory.CreateDirectory(Path.Combine(targetDir, entry.FileName)); - } - else - { - using (var ostream = new MemoryStream((int)entry.UncompressedSize)) - { - entry.Extract(ostream); - ostream.Seek(0, SeekOrigin.Begin); - - var md5 = new MD5CryptoServiceProvider(); - var fileHash = md5.ComputeHash(ostream); - - try - { - if (!Utils.UnsafeCompare(fileHash, fileInfo.Hashes.Where(h => h.File == entry.FileName).Select(h => h.Hash).First())) - throw new Exception("The hash for the file doesn't match what is defined"); - } - catch (KeyNotFoundException) - { - throw new BeatmodsInterceptException("BeatMods did not send the hashes for the zip's content!"); - } - - ostream.Seek(0, SeekOrigin.Begin); - FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName)); - Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException()); - - if (item.LocalPluginMeta != null && - Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath)) - shouldDeleteOldFile = false; // overwriting old file, no need to delete - - /*if (targetFile.Exists) - backup.Add(targetFile); - else - newFiles.Add(targetFile);*/ - - Logger.updater.Debug($"Extracting file {targetFile.FullName}"); - - targetFile.Delete(); - using (var fstream = targetFile.Create()) - ostream.CopyTo(fstream); - } - } - } - } - - if (shouldDeleteOldFile && item.LocalPluginMeta != null) - File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath) }); - } - catch (Exception) - { // something failed; restore - Directory.Delete(targetDir, true); // delete extraction site - - throw; - } - - if ((item.LocalPluginMeta?.Metadata.IsSelf).Unwrap()) - { // currently updating self, so copy to working dir and update - NeedsManualRestart = true; // flag so that ModList keeps the restart button hidden - Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(BeatSaber.InstallPath)); - var deleteFile = Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile); - if (File.Exists(deleteFile)) File.Delete(deleteFile); - Process.Start(new ProcessStartInfo - { - // will never actually be null - FileName = item.LocalPluginMeta?.Metadata.File.FullName ?? throw new InvalidOperationException(), - Arguments = $"-nw={Process.GetCurrentProcess().Id}", - UseShellExecute = false - }); - } - else - Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile); - Directory.Delete(targetDir, true); // delete extraction site - - Logger.updater.Debug("Extractor exited"); - } - - internal static bool NeedsManualRestart = false; - } - - [Serializable] - internal class NetworkException : Exception - { - public NetworkException() - { - } - - public NetworkException(string message) : base(message) - { - } - - public NetworkException(string message, Exception innerException) : base(message, innerException) - { - } - - protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } - - [Serializable] - internal class BeatmodsInterceptException : Exception - { - public BeatmodsInterceptException() - { - } - - public BeatmodsInterceptException(string message) : base(message) - { - } - - public BeatmodsInterceptException(string message, Exception innerException) : base(message, innerException) - { - } - - protected BeatmodsInterceptException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - } -#endif -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Ionic.Zip; +using IPA.Config; +using IPA.Loader; +using IPA.Loader.Features; +using IPA.Utilities; +using Newtonsoft.Json; +using SemVer; +using UnityEngine; +using UnityEngine.Networking; +using static IPA.Loader.PluginManager; +using Logger = IPA.Logging.Logger; +using Version = SemVer.Version; + +namespace IPA.Updating.BeatMods +{ + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal partial class Updater : MonoBehaviour + { + internal const string SpecialDeletionsFile = "$$delete"; + } + +#if BeatSaber + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + internal partial class Updater : MonoBehaviour + { + public static Updater Instance; + + internal static bool ModListPresent = false; + + public void Awake() + { + try + { + if (Instance != null) + Destroy(this); + else + { + Instance = this; + DontDestroyOnLoad(this); + if (!ModListPresent && SelfConfig.Updates_.AutoCheckUpdates_) + CheckForUpdates(); + } + } + catch (Exception e) + { + Logger.updater.Error(e); + } + } + + internal delegate void CheckUpdatesComplete(List toUpdate); + + public void CheckForUpdates(CheckUpdatesComplete onComplete = null) => StartCoroutine(CheckForUpdatesCoroutine(onComplete)); + + internal class DependencyObject + { + public string Name { get; set; } + public Version Version { get; set; } + public Version ResolvedVersion { get; set; } + public Range Requirement { get; set; } + public Range Conflicts { get; set; } // a range of versions that are not allowed to be downloaded + public bool Resolved { get; set; } + public bool Has { get; set; } + public HashSet Consumers { get; set; } = new HashSet(); + + public bool MetaRequestFailed { get; set; } + + public PluginMetadata LocalPluginMeta { get; set; } + + public bool IsLegacy { get; set; } = false; + + public override string ToString() + { + return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement} ! {Conflicts}) {(Has ? " Already have" : "")}"; + } + } + + public static void ResetRequestCache() + { + requestCache.Clear(); + modCache.Clear(); + modVersionsCache.Clear(); + } + + private static readonly Dictionary requestCache = new Dictionary(); + private static IEnumerator GetBeatModsEndpoint(string url, Ref result) + { + if (requestCache.TryGetValue(url, out string value)) + { + result.Value = value; + } + else + { + using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + url)) + { + yield return request.SendWebRequest(); + + if (request.isNetworkError) + { + result.Error = new NetworkException($"Network error while trying to download: {request.error}"); + yield break; + } + if (request.isHttpError) + { + if (request.responseCode == 404) + { + result.Error = new NetworkException("Not found"); + yield break; + } + + result.Error = new NetworkException($"Server returned error {request.error} while getting data"); + yield break; + } + + result.Value = request.downloadHandler.text; + + requestCache[url] = result.Value; + } + } + } + + private static readonly Dictionary modCache = new Dictionary(); + internal static IEnumerator GetModInfo(string modName, string ver, Ref result) + { + var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeDataString(modName), Uri.EscapeDataString(ver)); + + if (modCache.TryGetValue(uri, out ApiEndpoint.Mod value)) + { + result.Value = value; + } + else + { + Ref reqResult = new Ref(""); + + yield return GetBeatModsEndpoint(uri, reqResult); + + try + { + result.Value = JsonConvert.DeserializeObject>(reqResult.Value).First(); + + modCache[uri] = result.Value; + } + catch (Exception e) + { + result.Error = new Exception("Error decoding response", e); + } + } + } + + private static readonly Dictionary> modVersionsCache = new Dictionary>(); + internal static IEnumerator GetModVersionsMatching(string modName, Range range, Ref> result) + { + var uri = string.Format(ApiEndpoint.GetModsByName, Uri.EscapeDataString(modName)); + + if (modVersionsCache.TryGetValue(uri, out List value)) + { + result.Value = value; + } + else + { + Ref reqResult = new Ref(""); + + yield return GetBeatModsEndpoint(uri, reqResult); + + try + { + result.Value = JsonConvert.DeserializeObject>(reqResult.Value) + .Where(m => range.IsSatisfied(m.Version)).ToList(); + + modVersionsCache[uri] = result.Value; + } + catch (Exception e) + { + result.Error = new Exception("Error decoding response", e); + } + } + } + + internal IEnumerator CheckForUpdatesCoroutine(CheckUpdatesComplete onComplete) + { + var depList = new Ref>(new List()); + + foreach (var plugin in BSMetas) + { // initialize with data to resolve (1.1) + if (plugin.Metadata.Id != null) + { // updatable + var msinfo = plugin.Metadata; + var dep = new DependencyObject + { + Name = msinfo.Id, + Version = msinfo.Version, + Requirement = new Range($">={msinfo.Version}"), + LocalPluginMeta = msinfo + }; + + if (msinfo.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) + { // disable updating, by only matching self, so that dependencies can still be resolved + dep.Requirement = new Range(msinfo.Version.ToString()); + } + + depList.Value.Add(dep); + } + } + + foreach (var meta in PluginLoader.ignoredPlugins.Keys) + { // update ignored + if (meta.Id != null) + { // updatable + var dep = new DependencyObject + { + Name = meta.Id, + Version = meta.Version, + Requirement = new Range($">={meta.Version}"), + LocalPluginMeta = meta + }; + + if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) + { // disable updating, by only matching self + dep.Requirement = new Range(meta.Version.ToString()); + } + + depList.Value.Add(dep); + } + } + + foreach (var meta in DisabledPlugins) + { // update ignored + if (meta.Id != null) + { // updatable + var dep = new DependencyObject + { + Name = meta.Id, + Version = meta.Version, + Requirement = new Range($">={meta.Version}"), + LocalPluginMeta = meta + }; + + if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null) + { // disable updating, by only matching self + dep.Requirement = new Range(meta.Version.ToString()); + } + + depList.Value.Add(dep); + } + } + +#pragma warning disable CS0618 // Type or member is obsolete + foreach (var plug in Plugins) + { // throw these in the updater on the off chance that they are set up properly + try + { + var dep = new DependencyObject + { + Name = plug.Name, + Version = new Version(plug.Version), + Requirement = new Range($">={plug.Version}"), + IsLegacy = true, + LocalPluginMeta = null + }; + + depList.Value.Add(dep); + } + catch (Exception e) + { + Logger.updater.Warn($"Error trying to add legacy plugin {plug.Name} to updater"); + Logger.updater.Warn(e); + } + } +#pragma warning restore CS0618 // Type or member is obsolete + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Phantom Dependency: {dep}"); + + yield return ResolveDependencyRanges(depList); + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Dependency: {dep}"); + + yield return ResolveDependencyPresence(depList); + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Dependency: {dep}"); + + CheckDependencies(depList); + + onComplete?.Invoke(depList); + + if (!ModListPresent && SelfConfig.Updates_.AutoUpdate_) + StartDownload(depList.Value); + } + + internal IEnumerator ResolveDependencyRanges(Ref> list) + { + for (int i = 0; i < list.Value.Count; i++) + { // Grab dependencies (1.2) + var dep = list.Value[i]; + + var mod = new Ref(null); + + yield return GetModInfo(dep.Name, "", mod); + + try { mod.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error getting info for {dep.Name}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logger.updater.Error(e); + dep.MetaRequestFailed = true; + continue; + } + + list.Value.AddRange(mod.Value.Dependencies.Select(m => new DependencyObject + { + Name = m.Name, + Requirement = new Range($"^{m.Version}"), + Consumers = new HashSet { dep.Name } + })); + // currently no conflicts exist in BeatMods + //list.Value.AddRange(mod.Value.Links.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = d.VersionRange, Consumers = new HashSet { dep.Name } })); + //list.Value.AddRange(mod.Value.Links.Conflicts.Select(d => new DependencyObject { Name = d.Name, Conflicts = d.VersionRange, Consumers = new HashSet { dep.Name } })); + } + + var depNames = new HashSet(); + var final = new List(); + + foreach (var dep in list.Value) + { // agregate ranges and the like (1.3) + if (!depNames.Contains(dep.Name)) + { // should add it + depNames.Add(dep.Name); + final.Add(dep); + } + else + { + var toMod = final.First(d => d.Name == dep.Name); + + if (dep.Requirement != null) + { + toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement); + foreach (var consume in dep.Consumers) + toMod.Consumers.Add(consume); + } + if (dep.Conflicts != null) + { + toMod.Conflicts = toMod.Conflicts == null + ? dep.Conflicts + : new Range($"{toMod.Conflicts} || {dep.Conflicts}"); + } + } + } + + list.Value = final; + } + + internal IEnumerator ResolveDependencyPresence(Ref> list) + { + foreach(var dep in list.Value) + { + dep.Has = dep.Version != null; // dep.Version is only not null if its already installed + + if (dep.MetaRequestFailed) + { + Logger.updater.Warn($"{dep.Name} info request failed, not trying again"); + continue; + } + + var modsMatching = new Ref>(null); + yield return GetModVersionsMatching(dep.Name, dep.Requirement, modsMatching); + try { modsMatching.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error getting mod list for {dep.Name}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logger.updater.Error(e); + dep.MetaRequestFailed = true; + continue; + } + + var ver = modsMatching.Value + .NonNull() // entry is not null + .Where(versionCheck => versionCheck.GameVersion == BeatSaber.GameVersion) // game version matches + .Where(approvalCheck => approvalCheck.Status == ApiEndpoint.Mod.ApprovedStatus) // version approved + // TODO: fix; it seems wrong somehow + .Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version)) // not a conflicting version + .Select(mod => mod.Version).Max(); // (2.1) get the max version + dep.Resolved = ver != null; + if (dep.Resolved) dep.ResolvedVersion = ver; // (2.2) + dep.Has = dep.Resolved && dep.Version == dep.ResolvedVersion; + } + } + + internal void CheckDependencies(Ref> list) + { + var toDl = new List(); + + foreach (var dep in list.Value) + { // figure out which ones need to be downloaded (3.1) + if (dep.Resolved) + { + Logger.updater.Debug($"Resolved: {dep}"); + if (!dep.Has) + { + Logger.updater.Debug($"To Download: {dep}"); + toDl.Add(dep); + } + } + else if (!dep.Has) + { + if (dep.Version != null && dep.Requirement.IsSatisfied(dep.Version)) + Logger.updater.Notice($"Mod {dep.Name} running a newer version than is on BeatMods ({dep.Version})"); + else + Logger.updater.Warn($"Could not resolve dependency {dep}"); + } + } + + Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}"); + + list.Value = toDl; + } + + internal delegate void DownloadStart(DependencyObject obj); + internal delegate void DownloadProgress(DependencyObject obj, long totalBytes, long currentBytes, double progress); + internal delegate void DownloadFailed(DependencyObject obj, string error); + internal delegate void DownloadFinish(DependencyObject obj); + /// + /// This will still be called even if there was an error. Called after all three download/install attempts, or after a successful installation. + /// ALWAYS called. + /// + /// + /// + internal delegate void InstallFinish(DependencyObject obj, bool didError); + /// + /// This can be called multiple times + /// + /// + /// + internal delegate void InstallFailed(DependencyObject obj, Exception error); + + internal void StartDownload(IEnumerable download, DownloadStart downloadStart = null, + DownloadProgress downloadProgress = null, DownloadFailed downloadFail = null, DownloadFinish downloadFinish = null, + InstallFailed installFail = null, InstallFinish installFinish = null) + { + foreach (var item in download) + StartCoroutine(UpdateModCoroutine(item, downloadStart, downloadProgress, downloadFail, downloadFinish, installFail, installFinish)); + } + + private static IEnumerator UpdateModCoroutine(DependencyObject item, DownloadStart downloadStart, + DownloadProgress progress, DownloadFailed dlFail, DownloadFinish finish, + InstallFailed installFail, InstallFinish installFinish) + { // (3.2) + Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}"); + + var mod = new Ref(null); + yield return GetModInfo(item.Name, item.ResolvedVersion.ToString(), mod); + try { mod.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error occurred while trying to get information for {item}"); + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logger.updater.Error(e); + yield break; + } + + var releaseName = BeatSaber.ReleaseType == BeatSaber.Release.Steam + ? ApiEndpoint.Mod.DownloadsObject.TypeSteam : ApiEndpoint.Mod.DownloadsObject.TypeOculus; + var platformFile = mod.Value.Downloads.First(f => f.Type == ApiEndpoint.Mod.DownloadsObject.TypeUniversal || f.Type == releaseName); + + string url = ApiEndpoint.BeatModBase + platformFile.Path; + + Logger.updater.Debug($"URL = {url}"); + + const int maxTries = 3; + int tries = maxTries; + while (tries > 0) + { + if (tries-- != maxTries) + Logger.updater.Debug("Re-trying download..."); + + using (var stream = new MemoryStream()) + using (var request = UnityWebRequest.Get(url)) + using (var taskTokenSource = new CancellationTokenSource()) + { + var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress?.Invoke(item, i1, i2, d)); + request.downloadHandler = dlh; + + downloadStart?.Invoke(item); + + Logger.updater.Debug("Sending request"); + //Logger.updater.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL"); + yield return request.SendWebRequest(); + Logger.updater.Debug("Download finished"); + + if (request.isNetworkError) + { + Logger.updater.Error("Network error while trying to update mod"); + Logger.updater.Error(request.error); + dlFail?.Invoke(item, request.error); + taskTokenSource.Cancel(); + continue; + } + if (request.isHttpError) + { + Logger.updater.Error("Server returned an error code while trying to update mod"); + Logger.updater.Error(request.error); + dlFail?.Invoke(item, request.error); + taskTokenSource.Cancel(); + continue; + } + + finish?.Invoke(item); + + stream.Seek(0, SeekOrigin.Begin); // reset to beginning + + var downloadTask = Task.Run(() => + { // use slightly more multi threaded approach than co-routines + // ReSharper disable once AccessToDisposedClosure + ExtractPluginAsync(stream, item, platformFile); + }, taskTokenSource.Token); + + while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted)) + yield return null; // pause co-routine until task is done + + if (downloadTask.IsFaulted) + { + if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException)) + { // any exception is an intercept exception + Logger.updater.Error($"BeatMods did not return expected data for {item.Name}"); + } + else + Logger.updater.Error($"Error downloading mod {item.Name}"); + + if (SelfConfig.Debug_.ShowHandledErrorStackTraces_) + Logger.updater.Error(downloadTask.Exception); + + installFail?.Invoke(item, downloadTask.Exception); + continue; + } + + break; + } + } + + if (tries == 0) + { + Logger.updater.Warn($"Plugin download failed {maxTries} times, not re-trying"); + + installFinish?.Invoke(item, true); + } + else + { + Logger.updater.Debug("Download complete"); + installFinish?.Invoke(item, false); + } + } + + internal class StreamDownloadHandler : DownloadHandlerScript + { + internal int length; + internal int cLen; + internal Action progress; + public MemoryStream Stream { get; set; } + + public StreamDownloadHandler(MemoryStream stream, Action progress = null) + { + Stream = stream; + this.progress = progress; + } + + protected override void ReceiveContentLength(int contentLength) + { + Stream.Capacity = length = contentLength; + cLen = 0; + Logger.updater.Debug($"Got content length: {contentLength}"); + } + + protected override void CompleteContent() + { + Logger.updater.Debug("Download complete"); + } + + protected override bool ReceiveData(byte[] rData, int dataLength) + { + if (rData == null || rData.Length < 1) + { + Logger.updater.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer"); + return false; + } + + cLen += dataLength; + + Stream.Write(rData, 0, dataLength); + + progress?.Invoke(length, cLen, ((double)cLen) / length); + + return true; + } + + protected override byte[] GetData() { return null; } + + protected override float GetProgress() + { + return 0f; + } + + public override string ToString() + { + return $"{base.ToString()} ({Stream})"; + } + } + + private static void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo) + { // (3.3) + Logger.updater.Debug($"Extracting ZIP file for {item.Name}"); + + /*var data = stream.GetBuffer(); + SHA1 sha = new SHA1CryptoServiceProvider(); + var hash = sha.ComputeHash(data); + if (!Utils.UnsafeCompare(hash, fileInfo.Hash)) + throw new Exception("The hash for the file doesn't match what is defined");*/ + + var targetDir = Path.Combine(BeatSaber.InstallPath, "IPA", Path.GetRandomFileName() + "_Pending"); + Directory.CreateDirectory(targetDir); + + var eventualOutput = Path.Combine(BeatSaber.InstallPath, "IPA", "Pending"); + if (!Directory.Exists(eventualOutput)) + Directory.CreateDirectory(eventualOutput); + + try + { + bool shouldDeleteOldFile = !(item.LocalPluginMeta?.IsSelf).Unwrap(); + + using (var zipFile = ZipFile.Read(stream)) + { + Logger.updater.Debug("Streams opened"); + foreach (var entry in zipFile) + { + if (entry.IsDirectory) + { + Logger.updater.Debug($"Creating directory {entry.FileName}"); + Directory.CreateDirectory(Path.Combine(targetDir, entry.FileName)); + } + else + { + using (var ostream = new MemoryStream((int)entry.UncompressedSize)) + { + entry.Extract(ostream); + ostream.Seek(0, SeekOrigin.Begin); + + var md5 = new MD5CryptoServiceProvider(); + var fileHash = md5.ComputeHash(ostream); + + try + { + if (!Utils.UnsafeCompare(fileHash, fileInfo.Hashes.Where(h => h.File == entry.FileName).Select(h => h.Hash).First())) + throw new Exception("The hash for the file doesn't match what is defined"); + } + catch (KeyNotFoundException) + { + throw new BeatmodsInterceptException("BeatMods did not send the hashes for the zip's content!"); + } + + ostream.Seek(0, SeekOrigin.Begin); + FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName)); + Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException()); + + if (item.LocalPluginMeta != null && + Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, BeatSaber.InstallPath)) + shouldDeleteOldFile = false; // overwriting old file, no need to delete + + /*if (targetFile.Exists) + backup.Add(targetFile); + else + newFiles.Add(targetFile);*/ + + Logger.updater.Debug($"Extracting file {targetFile.FullName}"); + + targetFile.Delete(); + using (var fstream = targetFile.Create()) + ostream.CopyTo(fstream); + } + } + } + } + + if (shouldDeleteOldFile && item.LocalPluginMeta != null) + File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.File.FullName, BeatSaber.InstallPath) }); + } + catch (Exception) + { // something failed; restore + Directory.Delete(targetDir, true); // delete extraction site + + throw; + } + + if ((item.LocalPluginMeta?.IsSelf).Unwrap()) + { // currently updating self, so copy to working dir and update + NeedsManualRestart = true; // flag so that ModList keeps the restart button hidden + Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(BeatSaber.InstallPath)); + var deleteFile = Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile); + if (File.Exists(deleteFile)) File.Delete(deleteFile); + Process.Start(new ProcessStartInfo + { + // will never actually be null + FileName = item.LocalPluginMeta?.File.FullName ?? throw new InvalidOperationException(), + Arguments = $"-nw={Process.GetCurrentProcess().Id}", + UseShellExecute = false + }); + } + else + Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile); + Directory.Delete(targetDir, true); // delete extraction site + + Logger.updater.Debug("Extractor exited"); + } + + internal static bool NeedsManualRestart = false; + } + + [Serializable] + internal class NetworkException : Exception + { + public NetworkException() + { + } + + public NetworkException(string message) : base(message) + { + } + + public NetworkException(string message, Exception innerException) : base(message, innerException) + { + } + + protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } + + [Serializable] + internal class BeatmodsInterceptException : Exception + { + public BeatmodsInterceptException() + { + } + + public BeatmodsInterceptException(string message) : base(message) + { + } + + public BeatmodsInterceptException(string message, Exception innerException) : base(message, innerException) + { + } + + protected BeatmodsInterceptException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +#endif +}