diff --git a/.gitignore b/.gitignore index f1e3d20e..988a2bcd 100644 --- a/.gitignore +++ b/.gitignore @@ -250,3 +250,4 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml +/MigrationBackup/d2a2abe6/IPA.Injector diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj index e8e044a1..3b616354 100644 --- a/IPA.Injector/IPA.Injector.csproj +++ b/IPA.Injector/IPA.Injector.csproj @@ -50,6 +50,7 @@ + @@ -70,11 +71,20 @@ Libraries\Mono\I18N.West.dll Always + + Libraries\Mono\Microsoft.CSharp.dll + Always + Libraries\Mono\System.Runtime.Serialization.dll Always + + + 1.2.0 + + diff --git a/IPA.Injector/Properties/AssemblyInfo.cs b/IPA.Injector/Properties/AssemblyInfo.cs index 4519cca1..3646f1f1 100644 --- a/IPA.Injector/Properties/AssemblyInfo.cs +++ b/IPA.Injector/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using IPA.Injector; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,6 +23,7 @@ using System.Runtime.InteropServices; // 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")] +[assembly: ForceAssemblyReference(typeof(SemVer.Version))] // Version information for an assembly consists of the following four values: // diff --git a/IPA.Injector/WtfThisDoesntNeedToExist.cs b/IPA.Injector/WtfThisDoesntNeedToExist.cs new file mode 100644 index 00000000..b8514af2 --- /dev/null +++ b/IPA.Injector/WtfThisDoesntNeedToExist.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.Injector +{ + [AttributeUsage(AttributeTargets.Assembly)] + internal class ForceAssemblyReferenceAttribute : Attribute + { + public ForceAssemblyReferenceAttribute(Type forcedType) + { + //not sure if these two lines are required since + //the type is passed to constructor as parameter, + //thus effectively being used + Action noop = _ => { }; + noop(forcedType); + } + } +} diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 46a172a7..b14f2b89 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -72,6 +72,8 @@ + + diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 1f25869c..c661a703 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -156,7 +156,7 @@ namespace IPA.Loader var selfPlugin = new BSPluginMeta { Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"), - Plugin = new SelfPlugin() + Plugin = SelfPlugin.Instance }; selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo; @@ -173,7 +173,7 @@ namespace IPA.Loader Logger.log.Info(exeName); Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}"); - Logger.log.Info($"Game version {UnityEngine.Application.version}"); + Logger.log.Info($"Game version {BeatSaber.GameVersion}"); Logger.log.Info("-----------------------------"); Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); Logger.log.Info("-----------------------------"); diff --git a/IPA.Loader/Updating/Backup/BackupUnit.cs b/IPA.Loader/Updating/Backup/BackupUnit.cs index efd96db7..f5b8f2e7 100644 --- a/IPA.Loader/Updating/Backup/BackupUnit.cs +++ b/IPA.Loader/Updating/Backup/BackupUnit.cs @@ -73,9 +73,11 @@ namespace IPA.Updating.Backup /// public void Add(FileInfo file) { - var relativePath = LoneFunctions.GetRelativePath(Environment.CurrentDirectory, file.FullName); + var relativePath = LoneFunctions.GetRelativePath(file.FullName, Environment.CurrentDirectory); var backupPath = new FileInfo(Path.Combine(_BackupPath.FullName, relativePath)); + Logger.updater.Debug($"rp={relativePath}, bp={backupPath}"); + if (_Files.Contains(relativePath)) { Logger.updater.Debug($"Skipping backup of {relativePath}"); diff --git a/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs b/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs index 7c1934b7..db302142 100644 --- a/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs +++ b/IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs @@ -21,7 +21,7 @@ namespace IPA.Updating.ModsaberML public const string GetApprovedEndpoint = "updater_test.json"; #else public const string ApiBase = "https://www.modsaber.ml/"; - public const string GetApprovedEndpoint = "registry/{0}"; + public const string GetApprovedEndpoint = "registry/{0}/{1}"; #endif class HexArrayConverter : JsonConverter @@ -117,6 +117,7 @@ namespace IPA.Updating.ModsaberML { [JsonProperty("steam")] public PlatformFile Steam = null; + [JsonProperty("oculus")] public PlatformFile Oculus = null; } @@ -133,6 +134,9 @@ namespace IPA.Updating.ModsaberML [JsonProperty("dependsOn", ItemConverterType = typeof(ModsaberDependencyConverter))] public Dependency[] Dependencies = new Dependency[0]; + [JsonProperty("oldVersions", ItemConverterType = typeof(SemverVersionConverter))] + public Version[] OldVersions = new Version[0]; + 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 index 4b171813..d6719c38 100644 --- a/IPA.Loader/Updating/ModsaberML/Updater.cs +++ b/IPA.Loader/Updating/ModsaberML/Updater.cs @@ -15,9 +15,13 @@ using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; +using SemVer; using Logger = IPA.Logging.Logger; using Version = SemVer.Version; using IPA.Updating.Backup; +using System.Runtime.Serialization; +using System.Reflection; +using static IPA.Loader.PluginManager; namespace IPA.Updating.ModsaberML { @@ -43,124 +47,327 @@ namespace IPA.Updating.ModsaberML } } - public void CheckForUpdates() + private void CheckForUpdates() { StartCoroutine(CheckForUpdatesCoroutine()); } - private class ParsedPluginMeta : PluginManager.BSPluginMeta + private class DependencyObject { - private Version _verCache = null; - public Version ModVersion - { - get - { - if (_verCache == null) - _verCache = new Version(ModsaberInfo.CurrentVersion); - return _verCache; - } - } + public string Name { get; set; } + public Version Version { get; set; } = null; + public Version ResolvedVersion { get; set; } = null; + public Range Requirement { get; set; } = null; + public bool Resolved { get; set; } = false; + public bool Has { get; set; } = false; + public HashSet Consumers { get; set; } = new HashSet(); - public ParsedPluginMeta(PluginManager.BSPluginMeta meta) + public BSPluginMeta LocalPluginMeta { get; set; } = null; + + public override string ToString() { - this.Plugin = meta.Plugin; - this.ModsaberInfo = meta.ModsaberInfo; - this.Filename = meta.Filename; + return $"{Name}@{Version}{(Resolved ? $" -> {ResolvedVersion}" : "")} - ({Requirement}) {(Has ? $" Already have" : "")}"; } } - private struct UpdateStruct + private Dictionary requestCache = new Dictionary(); + private IEnumerator DownloadModInfo(string name, string ver, Ref result) { - public ParsedPluginMeta plugin; - public ApiEndpoint.Mod externInfo; - } - - IEnumerator CheckForUpdatesCoroutine() - { - Logger.updater.Info("Checking for mod updates..."); - - var toUpdate = new List(); - var GameVersion = new Version(Application.version); + var uri = ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, name, ver); - foreach (var _plugin in PluginManager.BSMetas) + if (requestCache.TryGetValue(uri, out ApiEndpoint.Mod value)) { - var plugin = new ParsedPluginMeta(_plugin); - var info = plugin.ModsaberInfo; - if (info == null) continue; - - using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, info.InternalName))) + result.Value = value; + yield break; + } + else + { + using (var request = UnityWebRequest.Get(uri)) { yield return request.SendWebRequest(); if (request.isNetworkError) { - Logger.updater.Error("Network error while trying to update mods"); - Logger.updater.Error(request.error); - continue; + result.Error = new NetworkException($"Network error while trying to download: {request.error}"); + yield break; } if (request.isHttpError) { if (request.responseCode == 404) { - Logger.updater.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}"); - continue; + result.Error = new NetworkException("Not found"); + yield break; } - Logger.updater.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}"); - Logger.updater.Error(request.error); - continue; + result.Error = new NetworkException($"Server returned error {request.error} while getting data"); + yield break; } - var json = request.downloadHandler.text; - - ApiEndpoint.Mod modRegistry; try { - modRegistry = JsonConvert.DeserializeObject(json); - Logger.updater.Debug(modRegistry.ToString()); + result.Value = JsonConvert.DeserializeObject(request.downloadHandler.text); + + requestCache[uri] = result.Value; } catch (Exception e) { - Logger.updater.Error($"Parse error while trying to update mods"); + result.Error = new Exception("Error decoding response", e); + yield break; + } + } + } + } + + private IEnumerator CheckForUpdatesCoroutine() + { + var depList = new Ref>(new List()); + + foreach (var plugin in BSMetas) + { // initialize with data to resolve (1.1) + if (plugin.ModsaberInfo != null) + { // updatable + var msinfo = plugin.ModsaberInfo; + depList.Value.Add(new DependencyObject { + Name = msinfo.InternalName, + Version = new Version(msinfo.CurrentVersion), + Requirement = new Range($">={msinfo.CurrentVersion}"), + LocalPluginMeta = plugin + }); + } + } + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Phantom Dependency: {dep.ToString()}"); + + yield return DependencyResolveFirstPass(depList); + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Dependency: {dep.ToString()}"); + + yield return DependencyResolveSecondPass(depList); + + foreach (var dep in depList.Value) + Logger.updater.Debug($"Dependency: {dep.ToString()}"); + + DependendyResolveFinalPass(depList); + } + + private IEnumerator DependencyResolveFirstPass(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); + + #region TEMPORARY get latest // SHOULD BE GREATEST OF VERSION + yield return DownloadModInfo(dep.Name, "", mod); + #endregion + + try { mod.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error getting info for {dep.Name}"); + Logger.updater.Error(e); + continue; + } + + list.Value.AddRange(mod.Value.Dependencies.Select(d => new DependencyObject { Name = d.Name, Requirement = 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.Where(d => d.Name == dep.Name).First(); + + toMod.Requirement = toMod.Requirement.Intersect(dep.Requirement); + foreach (var consume in dep.Consumers) + toMod.Consumers.Add(consume); + } + } + + list.Value = final; + } + + private IEnumerator DependencyResolveSecondPass(Ref> list) + { + IEnumerator GetGameVersionMap(string modname, Ref> map) + { // gets map of mod version -> game version (2.0) + map.Value = new Dictionary(); + + var mod = new Ref(null); + yield return DownloadModInfo(modname, "", mod); + try { mod.Verify(); } + catch (Exception) + { + map.Value = null; + map.Error = new Exception($"Error getting info for {modname}", mod.Error); + yield break; + } + + map.Value.Add(mod.Value.Version, mod.Value.GameVersion); + + foreach (var ver in mod.Value.OldVersions) + { + yield return DownloadModInfo(modname, ver.ToString(), mod); + try { mod.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error getting info for {modname}v{ver}"); Logger.updater.Error(e); continue; } + map.Value.Add(mod.Value.Version, mod.Value.GameVersion); + } + } + + foreach(var dep in list.Value) + { + dep.Has = dep.Version != null;// dep.Version is only not null if its already installed + + var dict = new Ref>(null); + yield return GetGameVersionMap(dep.Name, dict); + try { dict.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error getting map for {dep.Name}"); + Logger.updater.Error(e); + continue; + } + + var ver = dep.Requirement.MaxSatisfying(dict.Value.Where(kvp => kvp.Value == BeatSaber.GameVersion).Select(kvp => kvp.Key)); // (2.1) + if (dep.Resolved = ver != null) dep.ResolvedVersion = ver; // (2.2) + dep.Has = dep.Version == dep.ResolvedVersion && dep.Resolved; // dep.Version is only not null if its already installed + } + } - Logger.updater.Debug($"Found Modsaber.ML registration for {plugin.Plugin.Name} ({info.InternalName})"); - Logger.updater.Debug($"Installed version: {plugin.ModVersion}; Latest version: {modRegistry.Version}"); - if (modRegistry.Version > plugin.ModVersion) + private void DependendyResolveFinalPass(Ref> list) + { // also starts download of mods + 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.ToString()}"); + if (!dep.Has) { - Logger.updater.Debug($"{plugin.Plugin.Name} needs an update!"); - if (modRegistry.GameVersion == GameVersion) - { - Logger.updater.Debug($"Queueing update..."); - toUpdate.Add(new UpdateStruct - { - plugin = plugin, - externInfo = modRegistry - }); - } - else - { - Logger.updater.Warn($"Update avaliable for {plugin.Plugin.Name}, but for a different Beat Saber version!"); - } + Logger.updater.Debug($"To Download: {dep.ToString()}"); + toDl.Add(dep); } } + else if (!dep.Has) + { + Logger.updater.Warn($"Could not resolve dependency {dep}"); + } } - Logger.updater.Info($"{toUpdate.Count} mods need updating"); - - if (toUpdate.Count == 0) yield break; + Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}"); string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName()); Directory.CreateDirectory(tempDirectory); - foreach (var item in toUpdate) - { + + Logger.updater.Debug($"Temp directory: {tempDirectory}"); + + foreach (var item in toDl) StartCoroutine(UpdateModCoroutine(item, tempDirectory)); + } + + private IEnumerator UpdateModCoroutine(DependencyObject item, string tempDirectory) + { // (3.2) + Logger.updater.Debug($"Release: {BeatSaber.ReleaseType}"); + + var mod = new Ref(null); + yield return DownloadModInfo(item.Name, item.ResolvedVersion.ToString(), mod); + try { mod.Verify(); } + catch (Exception e) + { + Logger.updater.Error($"Error occurred while trying to get information for {item}"); + Logger.updater.Error(e); + yield break; + } + + ApiEndpoint.Mod.PlatformFile platformFile; + if (BeatSaber.ReleaseType == BeatSaber.Release.Steam || mod.Value.Files.Oculus == null) + platformFile = mod.Value.Files.Steam; + else + platformFile = mod.Value.Files.Oculus; + + string url = platformFile.DownloadPath; + + Logger.updater.Debug($"URL = {url}"); + + const int MaxTries = 3; + int maxTries = MaxTries; + while (maxTries > 0) + { + if (maxTries-- != 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); + request.downloadHandler = dlh; + + 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); + taskTokenSource.Cancel(); + continue; + } + if (request.isHttpError) + { + Logger.updater.Error($"Server returned an error code while trying to update mod"); + Logger.updater.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.updater.Error($"Error downloading mod {item.Name}"); + Logger.updater.Error(downloadTask.Exception); + continue; + } + + break; + } } + + if (maxTries == 0) + Logger.updater.Warn($"Plugin download failed {MaxTries} times, not re-trying"); + else + Logger.updater.Debug("Download complete"); } - class StreamDownloadHandler : DownloadHandlerScript + internal class StreamDownloadHandler : DownloadHandlerScript { public MemoryStream Stream { get; set; } @@ -205,9 +412,9 @@ namespace IPA.Updating.ModsaberML } } - private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory) - { - Logger.updater.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}"); + private void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory) + { // (3.3) + Logger.updater.Debug($"Extracting ZIP file for {item.Name}"); var data = stream.GetBuffer(); SHA1 sha = new SHA1CryptoServiceProvider(); @@ -216,7 +423,7 @@ namespace IPA.Updating.ModsaberML 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}"); + var backup = new BackupUnit(tempDirectory, $"backup-{item.Name}"); try { @@ -248,7 +455,7 @@ namespace IPA.Updating.ModsaberML FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName)); Directory.CreateDirectory(targetFile.DirectoryName); - if (targetFile.FullName == item.plugin.Filename) + if (targetFile.FullName == item.LocalPluginMeta?.Filename) shouldDeleteOldFile = false; // overwriting old file, no need to delete if (targetFile.Exists) @@ -258,6 +465,7 @@ namespace IPA.Updating.ModsaberML Logger.updater.Debug($"Extracting file {targetFile.FullName}"); + targetFile.Delete(); var fstream = targetFile.Create(); ostream.CopyTo(fstream); } @@ -265,17 +473,17 @@ namespace IPA.Updating.ModsaberML } } - if (item.plugin.Plugin is SelfPlugin) + if (item.LocalPluginMeta?.Plugin is SelfPlugin) { // currently updating self Process.Start(new ProcessStartInfo { - FileName = item.plugin.Filename, - Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait", + FileName = item.LocalPluginMeta.Filename, + Arguments = $"-nw={Process.GetCurrentProcess().Id}", UseShellExecute = false }); } - else if (shouldDeleteOldFile) - File.Delete(item.plugin.Filename); + else if (shouldDeleteOldFile && item.LocalPluginMeta != null) + File.Delete(item.LocalPluginMeta.Filename); } catch (Exception) { // something failed; restore @@ -289,82 +497,28 @@ namespace IPA.Updating.ModsaberML backup.Delete(); - Logger.updater.Debug("Downloader exited"); + Logger.updater.Debug("Extractor exited"); } + } - IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory) + [Serializable] + internal class NetworkException : Exception + { + public NetworkException() { - Logger.updater.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.updater.Debug($"URL = {url}"); - - const int MaxTries = 3; - int maxTries = MaxTries; - while (maxTries > 0) - { - if (maxTries-- != MaxTries) - Logger.updater.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.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); - taskTokenSource.Cancel(); - continue; - } - if (request.isHttpError) - { - Logger.updater.Error($"Server returned an error code while trying to update mod"); - Logger.updater.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.updater.Error($"Error downloading mod {item.plugin.Plugin.Name}"); - Logger.updater.Error(downloadTask.Exception); - continue; - } + public NetworkException(string message) : base(message) + { + } - break; - } - } + public NetworkException(string message, Exception innerException) : base(message, innerException) + { + } - if (maxTries == 0) - Logger.updater.Warn($"Plugin download failed {MaxTries} times, not re-trying"); - else - Logger.updater.Debug("Download complete"); + protected NetworkException(SerializationInfo info, StreamingContext context) : base(info, context) + { } } + } diff --git a/IPA.Loader/Updating/SelfPlugin.cs b/IPA.Loader/Updating/SelfPlugin.cs index 57fe3bf1..4d1b7bd0 100644 --- a/IPA.Loader/Updating/SelfPlugin.cs +++ b/IPA.Loader/Updating/SelfPlugin.cs @@ -13,6 +13,8 @@ namespace IPA.Updating internal const string IPA_Name = "Beat Saber IPA"; internal const string IPA_Version = "3.10.0"; + public static SelfPlugin Instance { get; set; } = new SelfPlugin(); + public string Name => IPA_Name; public string Version => IPA_Version; diff --git a/IPA.Loader/Utilities/BeatSaber.cs b/IPA.Loader/Utilities/BeatSaber.cs new file mode 100644 index 00000000..bb6c9427 --- /dev/null +++ b/IPA.Loader/Utilities/BeatSaber.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SemVer; +using Version = SemVer.Version; + +namespace IPA.Utilities +{ + /// + /// Provides some basic utility methods and properties of Beat Saber + /// + public static class BeatSaber + { + private static Version _gameVersion = null; + /// + /// Provides the current game version + /// + public static Version GameVersion => _gameVersion ?? (_gameVersion = new Version(UnityEngine.Application.version)); + + /// + /// The different types of releases of the game. + /// + public enum Release + { + /// + /// Indicates a Steam release. + /// + Steam, + /// + /// Indicates an Oculus release. + /// + Oculus + } + private static Release? _releaseCache = null; + /// + /// Gets the type of this installation of Beat Saber + /// + public static Release ReleaseType => (_releaseCache ?? (_releaseCache = FindSteamVRAsset() ? Release.Steam : Release.Oculus)).Value; + + private static bool FindSteamVRAsset() + { + // these require assembly qualified names.... + var SteamVRCamera = Type.GetType("SteamVR_Camera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); + var SteamVRExternalCamera = Type.GetType("SteamVR_ExternalCamera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); + var 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/Utilities/Ref.cs b/IPA.Loader/Utilities/Ref.cs new file mode 100644 index 00000000..bd9fdd63 --- /dev/null +++ b/IPA.Loader/Utilities/Ref.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.Utilities +{ + /// + /// A class to store a reference for passing to methods which cannot take ref parameters. + /// + /// the type of the value + public class Ref + { + private T _value; + /// + /// The value of the reference + /// + public T Value + { + get + { + if (Error != null) throw Error; + return _value; + } + set => _value = value; + } + + private Exception _error = null; + /// + /// An exception that was generated while creating the value. + /// + public Exception Error + { + get + { + return _error; + } + set + { + value.SetStackTrace(new StackTrace(1)); + _error = value; + } + } + /// + /// Constructor. + /// + /// the initial value of the reference + public Ref(T reference) + { + _value = reference; + } + + /// + /// Throws error if one was set. + /// + public void Verify() + { + if (Error != null) throw new Exception("Found error", Error); + } + } + + internal static class ExceptionUtilities + { + private static readonly FieldInfo STACK_TRACE_STRING_FI = typeof(Exception).GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); + private static readonly Type TRACE_FORMAT_TI = Type.GetType("System.Diagnostics.StackTrace").GetNestedType("TraceFormat", BindingFlags.NonPublic); + private static readonly MethodInfo TRACE_TO_STRING_MI = typeof(StackTrace).GetMethod("ToString", BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { TRACE_FORMAT_TI }, null); + + public static Exception SetStackTrace(this Exception target, StackTrace stack) + { + var getStackTraceString = TRACE_TO_STRING_MI.Invoke(stack, new object[] { Enum.GetValues(TRACE_FORMAT_TI).GetValue(0) }); + STACK_TRACE_STRING_FI.SetValue(target, getStackTraceString); + return target; + } + } +} diff --git a/IPA.Loader/Utilities/SteamCheck.cs b/IPA.Loader/Utilities/SteamCheck.cs index 8f5f0dc8..c3976806 100644 --- a/IPA.Loader/Utilities/SteamCheck.cs +++ b/IPA.Loader/Utilities/SteamCheck.cs @@ -9,7 +9,8 @@ namespace IPA.Utilities /// /// Provides a utility to test if this is a Steam build of Beat Saber. /// - public static class SteamCheck + [Obsolete("Use BeatSaber.ReleaseType == BeatSaber.Release.Steam")] + internal static class SteamCheck { private static Type SteamVRCamera; private static Type SteamVRExternalCamera; diff --git a/Libs/Microsoft.CSharp.dll b/Libs/Microsoft.CSharp.dll new file mode 100644 index 00000000..bb34ef16 Binary files /dev/null and b/Libs/Microsoft.CSharp.dll differ