Browse Source

Added support for `links` section of manifest

pull/11/head
Anairkoen Schno 5 years ago
parent
commit
b31e87074f
10 changed files with 241 additions and 45 deletions
  1. +2
    -4
      BSIPA-ModList/Plugin.cs
  2. +79
    -2
      BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs
  3. +6
    -3
      BSIPA-ModList/UI/ViewControllers/ModListController.cs
  4. +7
    -1
      IPA.Loader/Config/SelfConfig.cs
  5. +1
    -0
      IPA.Loader/IPA.Loader.csproj
  6. +32
    -0
      IPA.Loader/JsonConverters/MultilineStringConverter.cs
  7. +1
    -2
      IPA.Loader/Loader/PluginComponent.cs
  8. +19
    -1
      IPA.Loader/Loader/PluginManifest.cs
  9. +6
    -1
      IPA.Loader/Loader/manifest.json
  10. +88
    -31
      IPA.Loader/Updating/BeatMods/Updater.cs

+ 2
- 4
BSIPA-ModList/Plugin.cs View File

@ -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)


+ 79
- 2
BSIPA-ModList/UI/ViewControllers/ModInfoViewController.cs View File

@ -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<RectTransform>("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<HorizontalLayoutGroup>().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<HorizontalLayoutGroup>().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<HorizontalLayoutGroup>().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


+ 6
- 3
BSIPA-ModList/UI/ViewControllers/ModListController.cs View File

@ -78,7 +78,8 @@ namespace BSIPA_ModList.UI
infoView = BeatSaberUI.CreateViewController<ModInfoViewController>();
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<ModInfoViewController>();
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<ModInfoViewController>();
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);


+ 7
- 1
IPA.Loader/Config/SelfConfig.cs View File

@ -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
{


+ 1
- 0
IPA.Loader/IPA.Loader.csproj View File

@ -63,6 +63,7 @@
<Compile Include="Config\ConfigProviders\JsonConfigProvider.cs" />
<Compile Include="Config\IConfigProvider.cs" />
<Compile Include="Config\SelfConfig.cs" />
<Compile Include="JsonConverters\MultilineStringConverter.cs" />
<Compile Include="Loader\Composite\CompositeBSPlugin.cs" />
<Compile Include="Loader\Features\AddInFeature.cs" />
<Compile Include="Loader\Features\ConfigProviderFeature.cs" />


+ 32
- 0
IPA.Loader/JsonConverters/MultilineStringConverter.cs View File

@ -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<string>
{
public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartArray)
{
var list = serializer.Deserialize<string[]>(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);
}
}
}

+ 1
- 2
IPA.Loader/Loader/PluginComponent.cs View File

@ -28,8 +28,7 @@ namespace IPA.Loader
ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins);
#pragma warning restore 618
if (SelfConfig.SelfConfigRef.Value.AutoUpdate)
gameObject.AddComponent<Updating.BeatMods.Updater>();
gameObject.AddComponent<Updating.BeatMods.Updater>();
bsPlugins.OnApplicationStart();
ipaPlugins.OnApplicationStart();


+ 19
- 1
IPA.Loader/Loader/PluginManifest.cs View File

@ -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();
}
}

+ 6
- 1
IPA.Loader/Loader/manifest.json View File

@ -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"
}
}

+ 88
- 31
IPA.Loader/Updating/BeatMods/Updater.cs View File

@ -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<DependencyObject> 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<string, string> requestCache = new Dictionary<string, string>();
private IEnumerator GetBeatModsEndpoint(string url, Ref<string> result)
private static readonly Dictionary<string, string> requestCache = new Dictionary<string, string>();
private static IEnumerator GetBeatModsEndpoint(string url, Ref<string> result)
{
if (requestCache.TryGetValue(url, out string value))
{
@ -106,8 +113,8 @@ namespace IPA.Updating.BeatMods
}
}
private readonly Dictionary<string, ApiEndpoint.Mod> modCache = new Dictionary<string, ApiEndpoint.Mod>();
private IEnumerator GetModInfo(string modName, string ver, Ref<ApiEndpoint.Mod> result)
private static readonly Dictionary<string, ApiEndpoint.Mod> modCache = new Dictionary<string, ApiEndpoint.Mod>();
private static IEnumerator GetModInfo(string modName, string ver, Ref<ApiEndpoint.Mod> result)
{
var uri = string.Format(ApiEndpoint.GetModInfoEndpoint, Uri.EscapeUriString(modName), Uri.EscapeUriString(ver));
@ -134,8 +141,8 @@ namespace IPA.Updating.BeatMods
}
}
private readonly Dictionary<string, List<ApiEndpoint.Mod>> modVersionsCache = new Dictionary<string, List<ApiEndpoint.Mod>>();
private IEnumerator GetModVersionsMatching(string modName, Range range, Ref<List<ApiEndpoint.Mod>> result)
private static readonly Dictionary<string, List<ApiEndpoint.Mod>> modVersionsCache = new Dictionary<string, List<ApiEndpoint.Mod>>();
private static IEnumerator GetModVersionsMatching(string modName, Range range, Ref<List<ApiEndpoint.Mod>> 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<List<DependencyObject>>(new List<DependencyObject>());
@ -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<DependencyObject>> list)
internal IEnumerator ResolveDependencyRanges(Ref<List<DependencyObject>> 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<DependencyObject>> list)
internal IEnumerator ResolveDependencyPresence(Ref<List<DependencyObject>> list)
{
foreach(var dep in list.Value)
{
@ -329,7 +341,7 @@ namespace IPA.Updating.BeatMods
}
}
private void DependendyResolveFinalPass(Ref<List<DependencyObject>> list)
internal void CheckDependencies(Ref<List<DependencyObject>> list)
{ // also starts download of mods
var toDl = new List<DependencyObject>();
@ -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);
/// <summary>
/// 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.
/// </summary>
/// <param name="obj"></param>
/// <param name="didError"></param>
internal delegate void InstallFinish(DependencyObject obj, bool didError);
/// <summary>
/// This can be called multiple times
/// </summary>
/// <param name="obj"></param>
/// <param name="error"></param>
internal delegate void InstallFailed(DependencyObject obj, Exception error);
internal void StartDownload(List<DependencyObject> 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<int, int, double> progress;
public MemoryStream Stream { get; set; }
public StreamDownloadHandler(MemoryStream stream)
public StreamDownloadHandler(MemoryStream stream, Action<int, int, double> 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}");


Loading…
Cancel
Save