From b31e87074fb6f94fec9e6e5d0c1bc508278939d6 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Sat, 20 Apr 2019 19:59:27 -0500 Subject: [PATCH] Added support for `links` section of manifest --- BSIPA-ModList/Plugin.cs | 6 +- .../ViewControllers/ModInfoViewController.cs | 81 +++++++++++- .../UI/ViewControllers/ModListController.cs | 9 +- IPA.Loader/Config/SelfConfig.cs | 8 +- IPA.Loader/IPA.Loader.csproj | 1 + .../MultilineStringConverter.cs | 32 +++++ IPA.Loader/Loader/PluginComponent.cs | 3 +- IPA.Loader/Loader/PluginManifest.cs | 20 ++- IPA.Loader/Loader/manifest.json | 7 +- IPA.Loader/Updating/BeatMods/Updater.cs | 119 +++++++++++++----- 10 files changed, 241 insertions(+), 45 deletions(-) create mode 100644 IPA.Loader/JsonConverters/MultilineStringConverter.cs diff --git a/BSIPA-ModList/Plugin.cs b/BSIPA-ModList/Plugin.cs index bd99ff60..a6067db1 100644 --- a/BSIPA-ModList/Plugin.cs +++ b/BSIPA-ModList/Plugin.cs @@ -1,12 +1,8 @@ using IPA; using UnityEngine.SceneManagement; using IPALogger = IPA.Logging.Logger; -using CustomUI.BeatSaber; using BSIPA_ModList.UI; -using CustomUI.MenuButton; -using UnityEngine.Events; using UnityEngine; -using System.Linq; namespace BSIPA_ModList { @@ -21,6 +17,8 @@ namespace BSIPA_ModList { Logger.log = logger; Logger.log.Debug("Init"); + + IPA.Updating.BeatMods.Updater.ModListPresent = true; } public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) diff --git a/BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs b/BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs index a889685c..dbee57b4 100644 --- a/BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs +++ b/BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs @@ -1,8 +1,10 @@ using CustomUI.BeatSaber; +using CustomUI.MenuButton; using CustomUI.Utilities; using IPA.Loader; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -22,9 +24,15 @@ namespace BSIPA_ModList.UI internal string Description; internal PluginLoader.PluginMetadata UpdateInfo; + private static RectTransform rowTransformOriginal; + private ModInfoView view; + private RectTransform rowTransform; + private Button linkHomeButton; + private Button linkSourceButton; + private Button linkDonateButton; - public void Init(Sprite icon, string name, string version, string author, string description, PluginLoader.PluginMetadata updateInfo) + public void Init(Sprite icon, string name, string version, string author, string description, PluginLoader.PluginMetadata updateInfo, PluginManifest.LinksObject links = null) { Logger.log.Debug($"init info view controller"); @@ -35,6 +43,9 @@ namespace BSIPA_ModList.UI Description = description; UpdateInfo = updateInfo; + if (rowTransformOriginal == null) + rowTransformOriginal = MenuButtonUI.Instance.GetPrivateField("menuButtonsOriginal"); + // i also have no clue why this is necessary rectTransform.anchorMin = new Vector2(0f, 0f); rectTransform.anchorMax = new Vector2(0.5f, 1f); @@ -47,10 +58,76 @@ namespace BSIPA_ModList.UI rt.SetParent(transform); rt.anchorMin = new Vector2(0f, 0f); rt.anchorMax = new Vector2(1f, 1f); - rt.anchoredPosition = new Vector2(0f, 0f); + rt.anchoredPosition = new Vector2(0f, 0); view.Init(this); go.SetActive(true); + + if (links != null) + { + rowTransform = Instantiate(rowTransformOriginal, rectTransform); + rowTransform.anchorMin = new Vector2(0f, 0f); + rowTransform.anchorMax = new Vector2(1f, .15f); + rowTransform.anchoredPosition = new Vector2(-3.5f, -2f); + + foreach (Transform child in rowTransform) + { + child.name = string.Empty; + Destroy(child.gameObject); + } + + if (links.ProjectHome != null) + { + linkHomeButton = BeatSaberUI.CreateUIButton(rowTransform, "QuitButton", buttonText: "Home", + onClick: () => Process.Start(links.ProjectHome.ToString())); + linkHomeButton.GetComponentInChildren().padding = new RectOffset(6, 6, 0, 0); + } + if (links.ProjectSource != null) + { + linkSourceButton = BeatSaberUI.CreateUIButton(rowTransform, "QuitButton", buttonText: "Source", + onClick: () => Process.Start(links.ProjectSource.ToString())); + linkSourceButton.GetComponentInChildren().padding = new RectOffset(6, 6, 0, 0); + } + if (links.Donate != null) + { + linkDonateButton = BeatSaberUI.CreateUIButton(rowTransform, "QuitButton", buttonText: "Donate", + onClick: () => Process.Start(links.Donate.ToString())); + linkDonateButton.GetComponentInChildren().padding = new RectOffset(6, 6, 0, 0); + } + } } + +#if DEBUG + public void Update() + { +#if ADJUST_INFO_BUTTON_UI_LINKS + RectTransform rt = rowTransform; + + if (rt == null) return; + + var cpos = rt.anchoredPosition; + if (Input.GetKey(KeyCode.LeftArrow)) + { + rt.anchoredPosition = new Vector2(cpos.x - .1f, cpos.y); + } + else if (Input.GetKey(KeyCode.RightArrow)) + { + rt.anchoredPosition = new Vector2(cpos.x + .1f, cpos.y); + } + else if (Input.GetKey(KeyCode.UpArrow)) + { + rt.anchoredPosition = new Vector2(cpos.x, cpos.y + .1f); + } + else if (Input.GetKey(KeyCode.DownArrow)) + { + rt.anchoredPosition = new Vector2(cpos.x, cpos.y - .1f); + } + else + return; + + Logger.log.Debug($"Position now at {rt.anchoredPosition}"); +#endif + } +#endif } internal class ModInfoView : MonoBehaviour diff --git a/BSIPA-ModList/UI/ViewControllers/ModListController.cs b/BSIPA-ModList/UI/ViewControllers/ModListController.cs index 29778374..26af6e76 100644 --- a/BSIPA-ModList/UI/ViewControllers/ModListController.cs +++ b/BSIPA-ModList/UI/ViewControllers/ModListController.cs @@ -78,7 +78,8 @@ namespace BSIPA_ModList.UI infoView = BeatSaberUI.CreateViewController(); infoView.Init(icon, Plugin.Metadata.Name, "v" + Plugin.Metadata.Version.ToString(), subtext, - desc, Plugin.Metadata.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin.Metadata : null); + desc, Plugin.Metadata.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin.Metadata : null, + Plugin.Metadata.Manifest.Links); } list.flow.SetSelected(infoView, immediate: list.flow.HasSelected); @@ -123,7 +124,8 @@ namespace BSIPA_ModList.UI infoView = BeatSaberUI.CreateViewController(); infoView.Init(icon, Plugin.Name, "v" + Plugin.Version.ToString(), authorText, - desc, Plugin.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin : null); + desc, Plugin.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin : null, + Plugin.Manifest.Links); } list.flow.SetSelected(infoView, immediate: list.flow.HasSelected); @@ -173,7 +175,8 @@ namespace BSIPA_ModList.UI infoView = BeatSaberUI.CreateViewController(); infoView.Init(icon, Plugin.Metadata.Name, "v" + Plugin.Metadata.Version.ToString(), subtext, - desc, Plugin.Metadata.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin.Metadata : null); + desc, Plugin.Metadata.Features.FirstOrDefault(f => f is NoUpdateFeature) != null ? Plugin.Metadata : null, + Plugin.Metadata.Manifest.Links); } list.flow.SetSelected(infoView, immediate: list.flow.HasSelected); diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index 76affea1..659b18ee 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -39,7 +39,13 @@ namespace IPA.Config public bool ApplyAntiYeet = false; - public bool AutoUpdate = true; + public class UpdateObject + { + public bool AutoUpdate = true; + public bool AutoCheckUpdates = true; + } + + public UpdateObject Updates = new UpdateObject(); public class DebugObject { diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 9fd4b414..a9ddfa42 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -63,6 +63,7 @@ + diff --git a/IPA.Loader/JsonConverters/MultilineStringConverter.cs b/IPA.Loader/JsonConverters/MultilineStringConverter.cs new file mode 100644 index 00000000..38ef93e4 --- /dev/null +++ b/IPA.Loader/JsonConverters/MultilineStringConverter.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IPA.JsonConverters +{ + internal class MultilineStringConverter : JsonConverter + { + public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartArray) + { + var list = serializer.Deserialize(reader); + return string.Join("\n", list); + } + else + return reader.Value as string; + } + + public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) + { + var list = value.Split('\n'); + if (list.Length == 1) + serializer.Serialize(writer, value); + else + serializer.Serialize(writer, list); + } + } +} diff --git a/IPA.Loader/Loader/PluginComponent.cs b/IPA.Loader/Loader/PluginComponent.cs index 53f8250a..f25f29db 100644 --- a/IPA.Loader/Loader/PluginComponent.cs +++ b/IPA.Loader/Loader/PluginComponent.cs @@ -28,8 +28,7 @@ namespace IPA.Loader ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins); #pragma warning restore 618 - if (SelfConfig.SelfConfigRef.Value.AutoUpdate) - gameObject.AddComponent(); + gameObject.AddComponent(); bsPlugins.OnApplicationStart(); ipaPlugins.OnApplicationStart(); diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs index e45c330f..2c14b53e 100644 --- a/IPA.Loader/Loader/PluginManifest.cs +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -1,7 +1,9 @@ using IPA.JsonConverters; using Newtonsoft.Json; using SemVer; +using System; using System.Collections.Generic; +using Version = SemVer.Version; namespace IPA.Loader { @@ -13,7 +15,7 @@ namespace IPA.Loader [JsonProperty("id", Required = Required.AllowNull)] public string Id; - [JsonProperty("description", Required = Required.Always)] + [JsonProperty("description", Required = Required.Always), JsonConverter(typeof(MultilineStringConverter))] public string Description; [JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] @@ -42,5 +44,21 @@ namespace IPA.Loader [JsonProperty("icon", Required = Required.DisallowNull)] public string IconPath = null; + + [Serializable] + public class LinksObject + { + [JsonProperty("project-home", Required = Required.DisallowNull)] + public Uri ProjectHome = null; + + [JsonProperty("project-source", Required = Required.DisallowNull)] + public Uri ProjectSource = null; + + [JsonProperty("donate", Required = Required.DisallowNull)] + public Uri Donate = null; + } + + [JsonProperty("links", Required = Required.DisallowNull)] + public LinksObject Links = new LinksObject(); } } \ No newline at end of file diff --git a/IPA.Loader/Loader/manifest.json b/IPA.Loader/Loader/manifest.json index b53f052b..04731e2d 100644 --- a/IPA.Loader/Loader/manifest.json +++ b/IPA.Loader/Loader/manifest.json @@ -15,5 +15,10 @@ "define-feature(add-in, IPA.Loader.Features.AddInFeature)", "define-feature(init-injector, IPA.Loader.Features.InitInjectorFeature)", "define-feature(config-provider, IPA.Loader.Features.ConfigProviderFeature)" - ] + ], + "links": { + "project-home": "https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded/wiki", + "project-source": "https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded", + "donate": "https://ko-fi.com/danike" + } } \ No newline at end of file diff --git a/IPA.Loader/Updating/BeatMods/Updater.cs b/IPA.Loader/Updating/BeatMods/Updater.cs index b7440012..a44b5071 100644 --- a/IPA.Loader/Updating/BeatMods/Updater.cs +++ b/IPA.Loader/Updating/BeatMods/Updater.cs @@ -10,6 +10,7 @@ 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; @@ -28,6 +29,8 @@ namespace IPA.Updating.BeatMods { public static Updater Instance; + internal static bool ModListPresent = false; + public void Awake() { try @@ -37,7 +40,9 @@ namespace IPA.Updating.BeatMods else { Instance = this; - CheckForUpdates(); + DontDestroyOnLoad(this); + if (!ModListPresent && SelfConfig.SelfConfigRef.Value.Updates.AutoCheckUpdates) + CheckForUpdates(); } } catch (Exception e) @@ -46,9 +51,11 @@ namespace IPA.Updating.BeatMods } } - public void CheckForUpdates() => StartCoroutine(CheckForUpdatesCoroutine()); + internal delegate void CheckUpdatesComplete(List toUpdate); + + public void CheckForUpdates(CheckUpdatesComplete onComplete = null) => StartCoroutine(CheckForUpdatesCoroutine(onComplete)); - private class DependencyObject + internal class DependencyObject { public string Name { get; set; } public Version Version { get; set; } @@ -69,8 +76,8 @@ namespace IPA.Updating.BeatMods } } - private readonly Dictionary requestCache = new Dictionary(); - private IEnumerator GetBeatModsEndpoint(string url, Ref result) + private static readonly Dictionary requestCache = new Dictionary(); + private static IEnumerator GetBeatModsEndpoint(string url, Ref result) { if (requestCache.TryGetValue(url, out string value)) { @@ -106,8 +113,8 @@ namespace IPA.Updating.BeatMods } } - private readonly Dictionary modCache = new Dictionary(); - private IEnumerator GetModInfo(string modName, string ver, Ref result) + private static readonly Dictionary modCache = new Dictionary(); + private static IEnumerator GetModInfo(string modName, string ver, Ref result) { var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeUriString(modName), Uri.EscapeUriString(ver)); @@ -134,8 +141,8 @@ namespace IPA.Updating.BeatMods } } - private readonly Dictionary> modVersionsCache = new Dictionary>(); - private IEnumerator GetModVersionsMatching(string modName, Range range, Ref> result) + private static readonly Dictionary> modVersionsCache = new Dictionary>(); + private static IEnumerator GetModVersionsMatching(string modName, Range range, Ref> result) { var uri = string.Format(ApiEndpoint.GetModsByName, Uri.EscapeUriString(modName)); @@ -163,7 +170,7 @@ namespace IPA.Updating.BeatMods } } - private IEnumerator CheckForUpdatesCoroutine() + internal IEnumerator CheckForUpdatesCoroutine(CheckUpdatesComplete onComplete) { var depList = new Ref>(new List()); @@ -219,20 +226,25 @@ namespace IPA.Updating.BeatMods foreach (var dep in depList.Value) Logger.updater.Debug($"Phantom Dependency: {dep}"); - yield return DependencyResolveFirstPass(depList); + yield return ResolveDependencyRanges(depList); foreach (var dep in depList.Value) Logger.updater.Debug($"Dependency: {dep}"); - yield return DependencyResolveSecondPass(depList); + yield return ResolveDependencyPresence(depList); foreach (var dep in depList.Value) Logger.updater.Debug($"Dependency: {dep}"); - DependendyResolveFinalPass(depList); + CheckDependencies(depList); + + onComplete?.Invoke(depList); + + if (!ModListPresent && SelfConfig.SelfConfigRef.Value.Updates.AutoUpdate) + StartDownload(depList); } - private IEnumerator DependencyResolveFirstPass(Ref> list) + internal IEnumerator ResolveDependencyRanges(Ref> list) { for (int i = 0; i < list.Value.Count; i++) { // Grab dependencies (1.2) @@ -294,7 +306,7 @@ namespace IPA.Updating.BeatMods list.Value = final; } - private IEnumerator DependencyResolveSecondPass(Ref> list) + internal IEnumerator ResolveDependencyPresence(Ref> list) { foreach(var dep in list.Value) { @@ -329,7 +341,7 @@ namespace IPA.Updating.BeatMods } } - private void DependendyResolveFinalPass(Ref> list) + internal void CheckDependencies(Ref> list) { // also starts download of mods var toDl = new List(); @@ -355,11 +367,38 @@ namespace IPA.Updating.BeatMods Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}"); - foreach (var item in toDl) - StartCoroutine(UpdateModCoroutine(item)); + 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(List 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 IEnumerator UpdateModCoroutine(DependencyObject item) + 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}"); @@ -373,13 +412,6 @@ namespace IPA.Updating.BeatMods yield break; } - /* - ApiEndpoint.Mod.DownloadsObject platformFile; - if (BeatSaber.ReleaseType == BeatSaber.Release.Steam || mod.Value.Files.Oculus == null) - platformFile = mod.Value.Files.Steam; - else - platformFile = mod.Value.Files.Oculus;*/ - 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); @@ -399,9 +431,11 @@ namespace IPA.Updating.BeatMods using (var request = UnityWebRequest.Get(url)) using (var taskTokenSource = new CancellationTokenSource()) { - var dlh = new StreamDownloadHandler(stream); + var dlh = new StreamDownloadHandler(stream, (int i1, int i2, double d) => progress(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(); @@ -411,6 +445,7 @@ namespace IPA.Updating.BeatMods { Logger.updater.Error("Network error while trying to update mod"); Logger.updater.Error(request.error); + dlFail?.Invoke(item, request.error); taskTokenSource.Cancel(); continue; } @@ -418,10 +453,13 @@ namespace IPA.Updating.BeatMods { 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(() => @@ -437,11 +475,13 @@ namespace IPA.Updating.BeatMods { if (downloadTask.Exception != null && downloadTask.Exception.InnerExceptions.Any(e => e is BeatmodsInterceptException)) { // any exception is an intercept exception - Logger.updater.Error($"Modsaber did not return expected data for {item.Name}"); + Logger.updater.Error($"BeatMods did not return expected data for {item.Name}"); } Logger.updater.Error($"Error downloading mod {item.Name}"); Logger.updater.Error(downloadTask.Exception); + + installFail?.Invoke(item, downloadTask.Exception); continue; } @@ -450,23 +490,35 @@ namespace IPA.Updating.BeatMods } 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) + public StreamDownloadHandler(MemoryStream stream, Action progress = null) { Stream = stream; + this.progress = progress; } protected override void ReceiveContentLength(int contentLength) { - Stream.Capacity = contentLength; + Stream.Capacity = length = contentLength; + cLen = 0; Logger.updater.Debug($"Got content length: {contentLength}"); } @@ -483,7 +535,12 @@ namespace IPA.Updating.BeatMods return false; } + cLen += dataLength; + Stream.Write(rData, 0, dataLength); + + progress?.Invoke(length, cLen, ((double)cLen) / length); + return true; } @@ -500,7 +557,7 @@ namespace IPA.Updating.BeatMods } } - private void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo) + private static void ExtractPluginAsync(MemoryStream stream, DependencyObject item, ApiEndpoint.Mod.DownloadsObject fileInfo) { // (3.3) Logger.updater.Debug($"Extracting ZIP file for {item.Name}");