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