From 43390c0c2892df22e01677fdaa51470110b252f5 Mon Sep 17 00:00:00 2001 From: Max Weber Date: Sat, 30 Nov 2019 22:25:37 -0700 Subject: [PATCH] runelite-client: Add External Plugin support --- .../java/net/runelite/client/RuneLite.java | 11 +- .../runelite/client/RuneLiteProperties.java | 9 + .../client/config/RuneLiteConfig.java | 4 +- .../client/events/ExternalPluginsChanged.java | 38 ++ .../ExternalPluginClassLoader.java | 41 ++ .../externalplugins/ExternalPluginClient.java | 137 +++++ .../ExternalPluginManager.java | 366 ++++++++++++ .../ExternalPluginManifest.java | 84 +++ .../client/plugins/PluginManager.java | 94 +-- .../client/plugins/config/ConfigPanel.java | 33 +- .../config/PluginConfigurationDescriptor.java | 29 +- .../client/plugins/config/PluginHubPanel.java | 559 ++++++++++++++++++ .../client/plugins/config/PluginListItem.java | 40 +- .../plugins/config/PluginListPanel.java | 42 +- .../net/runelite/client/rs/ClientLoader.java | 2 + .../runelite/client/ui/FatalErrorDialog.java | 2 +- .../{rs => util}/CountingInputStream.java | 6 +- .../{rs => util}/VerificationException.java | 2 +- .../externalplugins/externalplugins.crt | 19 + .../plugins/config/pluginhub_configure.png | Bin 0 -> 410 bytes .../client/plugins/config/pluginhub_help.png | Bin 0 -> 477 bytes .../plugins/config/pluginhub_missingicon.png | Bin 0 -> 764 bytes .../net/runelite/client/runelite.properties | 2 + 23 files changed, 1447 insertions(+), 73 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java create mode 100644 runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java rename runelite-client/src/main/java/net/runelite/client/{rs => util}/CountingInputStream.java (93%) rename runelite-client/src/main/java/net/runelite/client/{rs => util}/VerificationException.java (97%) create mode 100644 runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_missingicon.png diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index f70ba2dd63..cb1ffd5ee2 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -57,6 +57,7 @@ import net.runelite.client.game.ItemManager; import net.runelite.client.game.LootManager; import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.menus.MenuManager; +import net.runelite.client.externalplugins.ExternalPluginManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; @@ -80,6 +81,7 @@ public class RuneLite { public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File CACHE_DIR = new File(RUNELITE_DIR, "cache"); + public static final File PLUGINS_DIR = new File(RUNELITE_DIR, "plugins"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); @@ -90,6 +92,9 @@ public class RuneLite @Inject private PluginManager pluginManager; + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private EventBus eventBus; @@ -288,12 +293,13 @@ public class RuneLite // Load the plugins, but does not start them yet. // This will initialize configuration pluginManager.loadCorePlugins(); + externalPluginManager.loadExternalPlugins(); SplashScreen.stage(.70, null, "Finalizing configuration"); // Plugins have provided their config, so set default config // to main settings - pluginManager.loadDefaultPluginConfiguration(); + pluginManager.loadDefaultPluginConfiguration(null); // Start client session clientSessionManager.start(); @@ -309,6 +315,7 @@ public class RuneLite // Register event listeners eventBus.register(clientUI); eventBus.register(pluginManager); + eventBus.register(externalPluginManager); eventBus.register(overlayManager); eventBus.register(drawManager); eventBus.register(infoBoxManager); @@ -337,7 +344,7 @@ public class RuneLite } // Start plugins - pluginManager.startCorePlugins(); + pluginManager.startPlugins(); SplashScreen.stop(); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index 60355dee62..6408be3522 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; import javax.annotation.Nullable; +import okhttp3.HttpUrl; public class RuneLiteProperties { @@ -45,6 +46,8 @@ public class RuneLiteProperties private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; private static final String JAV_CONFIG = "runelite.jav_config"; private static final String JAV_CONFIG_BACKUP = "runelite.jav_config_backup"; + private static final String PLUGINHUB_BASE = "runelite.pluginhub.url"; + private static final String PLUGINHUB_VERSION = "runelite.pluginhub.version"; private static final Properties properties = new Properties(); @@ -130,4 +133,10 @@ public class RuneLiteProperties { return properties.getProperty(JAV_CONFIG_BACKUP); } + + public static HttpUrl getPluginHubBase() + { + String version = System.getProperty(PLUGINHUB_VERSION, properties.getProperty(PLUGINHUB_VERSION)); + return HttpUrl.parse(properties.get(PLUGINHUB_BASE) + "/" + version); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java index 2c4cf3597a..e28bcdcbf1 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/RuneLiteConfig.java @@ -28,9 +28,11 @@ import java.awt.Dimension; import net.runelite.api.Constants; import net.runelite.client.ui.ContainableFrame; -@ConfigGroup("runelite") +@ConfigGroup(RuneLiteConfig.GROUP_NAME) public interface RuneLiteConfig extends Config { + String GROUP_NAME = "runelite"; + @ConfigItem( keyName = "gameSize", name = "Game size", diff --git a/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java new file mode 100644 index 0000000000..f38490121e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/ExternalPluginsChanged.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.events; + +import java.util.List; +import lombok.Value; +import net.runelite.client.externalplugins.ExternalPluginManifest; + +/** + * Posted when an external plugin has been added, removed, or updated + */ +@Value +public class ExternalPluginsChanged +{ + private final List loadedManifest; +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java new file mode 100644 index 0000000000..054a7779b5 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClassLoader.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.externalplugins; + +import java.net.URL; +import java.net.URLClassLoader; +import lombok.Getter; + +class ExternalPluginClassLoader extends URLClassLoader +{ + @Getter + private final ExternalPluginManifest manifest; + + ExternalPluginClassLoader(ExternalPluginManifest manifest, URL[] urls) + { + super(urls, ExternalPluginClassLoader.class.getClassLoader()); + this.manifest = manifest; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java new file mode 100644 index 0000000000..cd023c8c3a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginClient.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.externalplugins; + +import com.google.common.reflect.TypeToken; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.List; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import net.runelite.client.RuneLiteProperties; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.client.util.VerificationException; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okio.BufferedSource; + +public class ExternalPluginClient +{ + private final OkHttpClient cachingClient; + + @Inject + public ExternalPluginClient(OkHttpClient cachingClient) + { + this.cachingClient = cachingClient; + } + + public List downloadManifest() throws IOException, VerificationException + { + HttpUrl manifest = RuneLiteProperties.getPluginHubBase() + .newBuilder() + .addPathSegments("manifest.js") + .build(); + try (Response res = cachingClient.newCall(new Request.Builder().url(manifest).build()).execute()) + { + if (res.code() != 200) + { + throw new IOException("Non-OK response code: " + res.code()); + } + + BufferedSource src = res.body().source(); + + byte[] signature = new byte[src.readInt()]; + src.readFully(signature); + + byte[] data = src.readByteArray(); + Signature s = Signature.getInstance("SHA256withRSA"); + s.initVerify(loadCertificate()); + s.update(data); + + if (!s.verify(signature)) + { + throw new VerificationException("Unable to verify external plugin manifest"); + } + + return RuneLiteAPI.GSON.fromJson(new String(data, StandardCharsets.UTF_8), + new TypeToken>() + { + }.getType()); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) + { + throw new RuntimeException(e); + } + } + + public BufferedImage downloadIcon(ExternalPluginManifest plugin) throws IOException + { + if (!plugin.hasIcon()) + { + return null; + } + + HttpUrl url = RuneLiteProperties.getPluginHubBase() + .newBuilder() + .addPathSegment(plugin.getInternalName()) + .addPathSegment(plugin.getCommit() + ".png") + .build(); + + try (Response res = cachingClient.newCall(new Request.Builder().url(url).build()).execute()) + { + byte[] bytes = res.body().bytes(); + // We don't stream so the lock doesn't block the edt trying to load something at the same time + synchronized (ImageIO.class) + { + return ImageIO.read(new ByteArrayInputStream(bytes)); + } + } + } + + private static Certificate loadCertificate() + { + try + { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + Certificate certificate = certFactory.generateCertificate(ExternalPluginClient.class.getResourceAsStream("externalplugins.crt")); + return certificate; + } + catch (CertificateException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java new file mode 100644 index 0000000000..531f443cf9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManager.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.externalplugins; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import com.google.common.hash.Hashing; +import com.google.common.hash.HashingInputStream; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.events.SessionClose; +import net.runelite.client.events.SessionOpen; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginInstantiationException; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.ui.SplashScreen; +import net.runelite.client.util.CountingInputStream; +import net.runelite.client.util.Text; +import net.runelite.client.util.VerificationException; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; + +@Singleton +@Slf4j +public class ExternalPluginManager +{ + private static final String PLUGIN_LIST_KEY = "externalPlugins"; + private static Class[] builtinExternals = null; + + @Inject + private ConfigManager configManager; + + @Inject + private ExternalPluginClient externalPluginClient; + + @Inject + private PluginManager pluginManager; + + @Inject + private ScheduledExecutorService executor; + + @Inject + private EventBus eventBus; + + public void loadExternalPlugins() throws PluginInstantiationException + { + refreshPlugins(); + + if (builtinExternals != null) + { + // builtin external's don't actually have a manifest or a separate classloader... + pluginManager.loadPlugins(Lists.newArrayList(builtinExternals), null); + } + } + + @Subscribe + public void onSessionOpen(SessionOpen event) + { + executor.submit(this::refreshPlugins); + } + + @Subscribe + public void onSessionClose(SessionClose event) + { + executor.submit(this::refreshPlugins); + } + + private void refreshPlugins() + { + Multimap loadedExternalPlugins = HashMultimap.create(); + for (Plugin p : pluginManager.getPlugins()) + { + ExternalPluginManifest m = getExternalPluginManifest(p.getClass()); + if (m != null) + { + loadedExternalPlugins.put(m, p); + } + } + + List installedIDs = getInstalledExternalPlugins(); + if (installedIDs.isEmpty() && loadedExternalPlugins.isEmpty()) + { + return; + } + + boolean startup = SplashScreen.isOpen(); + try + { + double splashStart = startup ? .60 : 0; + double splashLength = startup ? .10 : 1; + if (!startup) + { + SplashScreen.init(); + } + + SplashScreen.stage(splashStart, null, "Downloading external plugins"); + Set externalPlugins = new HashSet<>(); + + RuneLite.PLUGINS_DIR.mkdirs(); + + List manifestList; + try + { + manifestList = externalPluginClient.downloadManifest(); + Map manifests = manifestList + .stream().collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity())); + + Set needsDownload = new HashSet<>(); + Set keep = new HashSet<>(); + + for (String name : installedIDs) + { + ExternalPluginManifest manifest = manifests.get(name); + if (manifest != null) + { + externalPlugins.add(manifest); + + if (!manifest.isValid()) + { + needsDownload.add(manifest); + } + else + { + keep.add(manifest.getJarFile()); + } + } + } + + // delete old plugins + File[] files = RuneLite.PLUGINS_DIR.listFiles(); + if (files != null) + { + for (File fi : files) + { + if (!keep.contains(fi)) + { + fi.delete(); + } + } + } + + int toDownload = needsDownload.stream().mapToInt(ExternalPluginManifest::getSize).sum(); + int downloaded = 0; + + for (ExternalPluginManifest manifest : needsDownload) + { + HttpUrl url = RuneLiteProperties.getPluginHubBase().newBuilder() + .addPathSegment(manifest.getInternalName()) + .addPathSegment(manifest.getCommit() + ".jar") + .build(); + + try (Response res = RuneLiteAPI.CLIENT.newCall(new Request.Builder().url(url).build()).execute()) + { + int fdownloaded = downloaded; + downloaded += manifest.getSize(); + HashingInputStream his = new HashingInputStream(Hashing.sha256(), + new CountingInputStream(res.body().byteStream(), i -> + SplashScreen.stage(splashStart + (splashLength * .2), splashStart + (splashLength * .8), + null, "Downloading " + manifest.getDisplayName(), + i + fdownloaded, toDownload, true))); + Files.asByteSink(manifest.getJarFile()).writeFrom(his); + if (!his.hash().toString().equals(manifest.getHash())) + { + throw new VerificationException("Plugin " + manifest.getInternalName() + " didn't match its hash"); + } + } + catch (IOException | VerificationException e) + { + externalPlugins.remove(manifest); + log.error("Unable to download external plugin \"{}\"", manifest.getInternalName(), e); + } + } + } + catch (IOException | VerificationException e) + { + log.error("Unable to download external plugins", e); + return; + } + + SplashScreen.stage(splashStart + (splashLength * .8), null, "Starting external plugins"); + + // TODO(abex): make sure the plugins get fully removed from the scheduler/eventbus/other managers (iterate and check classloader) + Set add = new HashSet<>(); + for (ExternalPluginManifest ex : externalPlugins) + { + if (loadedExternalPlugins.removeAll(ex).size() <= 0) + { + add.add(ex); + } + } + // list of loaded external plugins that aren't in the manifest + Collection remove = loadedExternalPlugins.values(); + + for (Plugin p : remove) + { + log.info("Stopping external plugin \"{}\"", p.getClass()); + try + { + pluginManager.stopPlugin(p); + } + catch (PluginInstantiationException e) + { + log.warn("Unable to stop external plugin \"{}\"", p.getClass().getName(), e); + } + pluginManager.remove(p); + } + + for (ExternalPluginManifest manifest : add) + { + // I think this can't happen, but just in case + if (!manifest.isValid()) + { + log.warn("Invalid plugin for validated manifest: {}", manifest); + continue; + } + + log.info("Loading external plugin \"{}\" version \"{}\" commit \"{}\"", manifest.getInternalName(), manifest.getVersion(), manifest.getCommit()); + + List newPlugins = null; + try + { + ClassLoader cl = new ExternalPluginClassLoader(manifest, new URL[]{manifest.getJarFile().toURI().toURL()}); + List> clazzes = new ArrayList<>(); + for (String className : manifest.getPlugins()) + { + clazzes.add(cl.loadClass(className)); + } + + newPlugins = pluginManager.loadPlugins(clazzes, null); + if (!startup) + { + pluginManager.loadDefaultPluginConfiguration(newPlugins); + + for (Plugin p : newPlugins) + { + pluginManager.startPlugin(p); + } + } + } + catch (Exception e) + { + log.warn("Unable to start or load external plugin \"{}\"", manifest.getInternalName(), e); + if (newPlugins != null) + { + for (Plugin p : newPlugins) + { + try + { + pluginManager.stopPlugin(p); + } + catch (Exception inner) + { + } + pluginManager.remove(p); + } + } + } + } + + if (!startup) + { + eventBus.post(new ExternalPluginsChanged(manifestList)); + } + } + finally + { + if (!startup) + { + SplashScreen.stop(); + } + } + } + + public List getInstalledExternalPlugins() + { + String externalPluginsStr = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY); + return Text.fromCSV(externalPluginsStr == null ? "" : externalPluginsStr); + } + + public void install(String key) + { + Set plugins = new HashSet<>(getInstalledExternalPlugins()); + if (plugins.add(key)) + { + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins)); + executor.submit(this::refreshPlugins); + } + } + + public void remove(String key) + { + Set plugins = new HashSet<>(getInstalledExternalPlugins()); + if (plugins.remove(key)) + { + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins)); + executor.submit(this::refreshPlugins); + } + } + + public void update() + { + executor.submit(this::refreshPlugins); + } + + public static ExternalPluginManifest getExternalPluginManifest(Class plugin) + { + ClassLoader cl = plugin.getClassLoader(); + if (cl instanceof ExternalPluginClassLoader) + { + ExternalPluginClassLoader ecl = (ExternalPluginClassLoader) cl; + return ecl.getManifest(); + } + return null; + } + + public static void loadBuiltin(Class... plugins) + { + builtinExternals = plugins; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java new file mode 100644 index 0000000000..95b5fe5637 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/externalplugins/ExternalPluginManifest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.externalplugins; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.runelite.client.RuneLite; + +@Data +public class ExternalPluginManifest +{ + private String internalName; + private String commit; + private String hash; + private int size; + private String[] plugins; + + private String displayName; + private String version; + private String author; + private String description; + private String[] tags; + @EqualsAndHashCode.Exclude + private URL support; + private boolean hasIcon; + + public boolean hasIcon() + { + return hasIcon; + } + + File getJarFile() + { + return new File(RuneLite.PLUGINS_DIR, internalName + commit + ".jar"); + } + + boolean isValid() + { + File file = getJarFile(); + + try + { + if (file.exists()) + { + String hash = Files.asByteSource(file).hash(Hashing.sha256()).toString(); + if (this.hash.equals(hash)) + { + return true; + } + } + } + catch (IOException e) + { + } + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 25d379c998..fadd54ebc9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -25,7 +25,6 @@ package net.runelite.client.plugins; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.graph.Graph; import com.google.common.graph.GraphBuilder; @@ -49,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; @@ -61,7 +61,6 @@ import net.runelite.client.events.SessionClose; import net.runelite.client.events.SessionOpen; import net.runelite.client.RuneLite; import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; @@ -90,8 +89,6 @@ public class PluginManager private final Provider sceneTileManager; private final List plugins = new CopyOnWriteArrayList<>(); private final List activePlugins = new CopyOnWriteArrayList<>(); - private final String runeliteGroupName = RuneLiteConfig.class - .getAnnotation(ConfigGroup.class).value(); @Setter boolean isOutdated; @@ -128,15 +125,22 @@ public class PluginManager private void refreshPlugins() { - loadDefaultPluginConfiguration(); + loadDefaultPluginConfiguration(null); getPlugins() .forEach(plugin -> executor.submit(() -> { try { - if (!startPlugin(plugin)) + if (isPluginEnabled(plugin) != activePlugins.contains(plugin)) { - stopPlugin(plugin); + if (activePlugins.contains(plugin)) + { + stopPlugin(plugin); + } + else + { + startPlugin(plugin); + } } } catch (PluginInstantiationException e) @@ -162,11 +166,15 @@ public class PluginManager return null; } - public List getPluginConfigProxies() + public List getPluginConfigProxies(Collection plugins) { List injectors = new ArrayList<>(); - injectors.add(RuneLite.getInjector()); - getPlugins().forEach(pl -> injectors.add(pl.getInjector())); + if (plugins == null) + { + injectors.add(RuneLite.getInjector()); + plugins = getPlugins(); + } + plugins.forEach(pl -> injectors.add(pl.getInjector())); List list = new ArrayList<>(); for (Injector injector : injectors) @@ -185,20 +193,15 @@ public class PluginManager return list; } - public void loadDefaultPluginConfiguration() + public void loadDefaultPluginConfiguration(Collection plugins) { - for (Object config : getPluginConfigProxies()) + for (Object config : getPluginConfigProxies(plugins)) { configManager.setDefaultConfiguration(config, false); } } - public void loadCorePlugins() throws IOException - { - plugins.addAll(scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE)); - } - - public void startCorePlugins() + public void startPlugins() { List scannedPlugins = new ArrayList<>(plugins); int loaded = 0; @@ -219,37 +222,41 @@ public class PluginManager } } - List scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException + public void loadCorePlugins() throws IOException, PluginInstantiationException { SplashScreen.stage(.59, null, "Loading Plugins"); + ClassPath classPath = ClassPath.from(getClass().getClassLoader()); + + List> plugins = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE).stream() + .map(ClassInfo::load) + .collect(Collectors.toList()); + + loadPlugins(plugins, (loaded, total) -> + SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, total, false)); + } + + public List loadPlugins(List> plugins, BiConsumer onPluginLoaded) throws PluginInstantiationException + { MutableGraph> graph = GraphBuilder .directed() .build(); - List scannedPlugins = new ArrayList<>(); - ClassPath classPath = ClassPath.from(classLoader); - - ImmutableSet classes = packageName == null ? classPath.getAllClasses() - : classPath.getTopLevelClassesRecursive(packageName); - for (ClassInfo classInfo : classes) + for (Class clazz : plugins) { - Class clazz = classInfo.load(); PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class); if (pluginDescriptor == null) { if (clazz.getSuperclass() == Plugin.class) { - log.warn("Class {} is a plugin, but has no plugin descriptor", - clazz); + log.warn("Class {} is a plugin, but has no plugin descriptor", clazz); } continue; } if (clazz.getSuperclass() != Plugin.class) { - log.warn("Class {} has plugin descriptor, but is not a plugin", - clazz); + log.warn("Class {} has plugin descriptor, but is not a plugin", clazz); continue; } @@ -280,20 +287,22 @@ public class PluginManager if (Graphs.hasCycle(graph)) { - throw new RuntimeException("Plugin dependency graph contains a cycle!"); + throw new PluginInstantiationException("Plugin dependency graph contains a cycle!"); } List> sortedPlugins = topologicalSort(graph); sortedPlugins = Lists.reverse(sortedPlugins); int loaded = 0; + List newPlugins = new ArrayList<>(); for (Class pluginClazz : sortedPlugins) { Plugin plugin; try { - plugin = instantiate(scannedPlugins, (Class) pluginClazz); - scannedPlugins.add(plugin); + plugin = instantiate(this.plugins, (Class) pluginClazz); + newPlugins.add(plugin); + this.plugins.add(plugin); } catch (PluginInstantiationException ex) { @@ -301,10 +310,13 @@ public class PluginManager } loaded++; - SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, sortedPlugins.size(), false); + if (onPluginLoaded != null) + { + onPluginLoaded.accept(loaded, sortedPlugins.size()); + } } - return scannedPlugins; + return newPlugins; } public synchronized boolean startPlugin(Plugin plugin) throws PluginInstantiationException @@ -355,13 +367,11 @@ public class PluginManager public synchronized boolean stopPlugin(Plugin plugin) throws PluginInstantiationException { - if (!activePlugins.contains(plugin) || isPluginEnabled(plugin)) + if (!activePlugins.remove(plugin)) { return false; } - activePlugins.remove(plugin); - try { unschedule(plugin); @@ -395,13 +405,13 @@ public class PluginManager public void setPluginEnabled(Plugin plugin, boolean enabled) { final String keyName = plugin.getClass().getSimpleName().toLowerCase(); - configManager.setConfiguration(runeliteGroupName, keyName, String.valueOf(enabled)); + configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, keyName, String.valueOf(enabled)); } public boolean isPluginEnabled(Plugin plugin) { final String keyName = plugin.getClass().getSimpleName().toLowerCase(); - final String value = configManager.getConfiguration(runeliteGroupName, keyName); + final String value = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, keyName); if (value != null) { @@ -465,12 +475,12 @@ public class PluginManager return plugin; } - void add(Plugin plugin) + public void add(Plugin plugin) { plugins.add(plugin); } - void remove(Plugin plugin) + public void remove(Plugin plugin) { plugins.remove(plugin); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index 4ed399b78a..01989d39d9 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java @@ -44,6 +44,7 @@ import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFormattedTextField; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; @@ -66,7 +67,10 @@ import net.runelite.client.config.Keybind; import net.runelite.client.config.ModifierlessKeybind; import net.runelite.client.config.Range; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; import net.runelite.client.events.PluginChanged; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; @@ -83,8 +87,8 @@ import net.runelite.client.util.Text; class ConfigPanel extends PluginPanel { private static final int SPINNER_FIELD_WIDTH = 6; - private static final ImageIcon BACK_ICON; - private static final ImageIcon BACK_ICON_HOVER; + static final ImageIcon BACK_ICON; + static final ImageIcon BACK_ICON_HOVER; private final FixedWidthPanel mainPanel; private final JLabel title; @@ -99,6 +103,9 @@ class ConfigPanel extends PluginPanel @Inject private PluginManager pluginManager; + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private ColorPickerManager colorPickerManager; @@ -162,7 +169,16 @@ class ConfigPanel extends PluginPanel title.setText(name); title.setForeground(Color.WHITE); title.setToolTipText("" + name + ":
" + pluginConfig.getDescription() + ""); - PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem()); + + ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest(); + JMenuItem uninstallItem = null; + if (mf != null) + { + uninstallItem = new JMenuItem("Uninstall"); + uninstallItem.addActionListener(ev -> externalPluginManager.remove(mf.getInternalName())); + } + + PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem(), uninstallItem); if (pluginConfig.getPlugin() != null) { @@ -495,4 +511,15 @@ class ConfigPanel extends PluginPanel }); } } + + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + if (pluginManager.getPlugins().stream() + .noneMatch(p -> p == this.pluginConfig.getPlugin())) + { + pluginList.getMuxer().popState(); + } + SwingUtilities.invokeLater(this::rebuild); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java index 69932e1d61..279991656b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginConfigurationDescriptor.java @@ -29,6 +29,8 @@ import javax.swing.JMenuItem; import lombok.Value; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.plugins.Plugin; import net.runelite.client.util.LinkBrowser; @@ -60,10 +62,35 @@ class PluginConfigurationDescriptor * * @return A {@link JMenuItem} which opens the plugin's wiki page URL in the browser when clicked */ + @Nullable JMenuItem createSupportMenuItem() { - final JMenuItem menuItem = new JMenuItem("Wiki"); + ExternalPluginManifest mf = getExternalPluginManifest(); + if (mf != null) + { + if (mf.getSupport() == null) + { + return null; + } + + JMenuItem menuItem = new JMenuItem("Support"); + menuItem.addActionListener(e -> LinkBrowser.browse(mf.getSupport().toString())); + return menuItem; + } + + JMenuItem menuItem = new JMenuItem("Wiki"); menuItem.addActionListener(e -> LinkBrowser.browse("https://github.com/runelite/runelite/wiki/" + name.replace(' ', '-'))); return menuItem; } + + @Nullable + ExternalPluginManifest getExternalPluginManifest() + { + if (plugin == null) + { + return null; + } + + return ExternalPluginManager.getExternalPluginManifest(plugin.getClass()); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java new file mode 100644 index 0000000000..09907493ff --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginHubPanel.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2019 Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins.config; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; +import java.util.function.ToDoubleFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.GroupLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.KeyStroke; +import javax.swing.LayoutStyle; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.Config; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; +import net.runelite.client.externalplugins.ExternalPluginClient; +import net.runelite.client.externalplugins.ExternalPluginManager; +import net.runelite.client.externalplugins.ExternalPluginManifest; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.DynamicGridLayout; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.IconTextField; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.LinkBrowser; +import net.runelite.client.util.SwingUtil; +import net.runelite.client.util.VerificationException; +import org.apache.commons.text.similarity.JaroWinklerDistance; + +@Slf4j +@Singleton +class PluginHubPanel extends PluginPanel +{ + private static final ImageIcon MISSING_ICON; + private static final ImageIcon HELP_ICON; + private static final ImageIcon HELP_ICON_HOVER; + private static final ImageIcon CONFIGURE_ICON; + private static final ImageIcon CONFIGURE_ICON_HOVER; + private static final Pattern SPACES = Pattern.compile(" +"); + private static final JaroWinklerDistance DISTANCE = new JaroWinklerDistance(); + + static + { + BufferedImage missingIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_missingicon.png"); + MISSING_ICON = new ImageIcon(missingIcon); + + BufferedImage helpIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_help.png"); + HELP_ICON = new ImageIcon(helpIcon); + HELP_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(helpIcon, -100)); + + BufferedImage configureIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_configure.png"); + CONFIGURE_ICON = new ImageIcon(configureIcon); + CONFIGURE_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(configureIcon, -100)); + } + + private class PluginItem extends JPanel + { + private static final int HEIGHT = 70; + private static final int ICON_WIDTH = 48; + private static final int BOTTOM_LINE_HEIGHT = 16; + static final float MIN_FILTER_SCORE = .8f; + + private final ExternalPluginManifest manifest; + + @Getter + private final boolean installed; + + @Getter + private float filter; + + PluginItem(ExternalPluginManifest newManifest, Collection loadedPlugins, boolean installed) + { + ExternalPluginManifest loaded = null; + if (!loadedPlugins.isEmpty()) + { + loaded = ExternalPluginManager.getExternalPluginManifest(loadedPlugins.iterator().next().getClass()); + } + + manifest = newManifest == null ? loaded : newManifest; + this.installed = installed; + + setBackground(ColorScheme.DARKER_GRAY_COLOR); + setOpaque(true); + + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + + JLabel pluginName = new JLabel(manifest.getDisplayName()); + pluginName.setFont(FontManager.getRunescapeBoldFont()); + pluginName.setToolTipText(manifest.getDisplayName()); + + JLabel author = new JLabel(manifest.getAuthor()); + author.setFont(FontManager.getRunescapeSmallFont()); + author.setToolTipText(manifest.getAuthor()); + + JLabel version = new JLabel(manifest.getVersion()); + version.setFont(FontManager.getRunescapeSmallFont()); + version.setToolTipText(manifest.getVersion()); + + JLabel description = new JLabel(manifest.getDescription()); + description.setToolTipText(manifest.getDescription()); + + JLabel icon = new JLabel(); + icon.setHorizontalAlignment(JLabel.CENTER); + icon.setIcon(MISSING_ICON); + if (manifest.hasIcon()) + { + executor.submit(() -> + { + try + { + BufferedImage img = externalPluginClient.downloadIcon(manifest); + + SwingUtilities.invokeLater(() -> + { + icon.setIcon(new ImageIcon(img)); + }); + } + catch (IOException e) + { + log.info("Cannot download icon for plugin \"{}\"", manifest.getInternalName(), e); + } + }); + } + + JButton help = new JButton(HELP_ICON); + help.setRolloverIcon(HELP_ICON_HOVER); + SwingUtil.removeButtonDecorations(help); + help.setToolTipText("Help"); + help.setBorder(null); + if (manifest.getSupport() == null) + { + help.setVisible(false); + } + else + { + help.addActionListener(ev -> LinkBrowser.browse(manifest.getSupport().toString())); + } + + JButton configure = new JButton(CONFIGURE_ICON); + configure.setRolloverIcon(CONFIGURE_ICON_HOVER); + SwingUtil.removeButtonDecorations(configure); + configure.setToolTipText("Configure"); + help.setBorder(null); + if (loaded != null) + { + String search = null; + if (loadedPlugins.size() > 1) + { + search = loaded.getInternalName(); + } + else + { + Plugin plugin = loadedPlugins.iterator().next(); + Config cfg = pluginManager.getPluginConfigProxy(plugin); + if (cfg == null) + { + search = loaded.getInternalName(); + } + else + { + configure.addActionListener(l -> pluginListPanel.openConfigurationPanel(plugin)); + } + } + + if (search != null) + { + final String javaIsABadLanguage = search; + configure.addActionListener(l -> pluginListPanel.openWithFilter(javaIsABadLanguage)); + } + } + else + { + configure.setVisible(false); + } + + boolean install = !installed; + boolean update = loaded != null && newManifest != null && !newManifest.equals(loaded); + boolean remove = !install && !update; + JButton addrm = new JButton(); + if (install) + { + addrm.setText("Install"); + addrm.setBackground(new Color(0x28BE28)); + addrm.addActionListener(l -> externalPluginManager.install(manifest.getInternalName())); + } + else if (remove) + { + addrm.setText("Remove"); + addrm.setBackground(new Color(0xBE2828)); + addrm.addActionListener(l -> externalPluginManager.remove(manifest.getInternalName())); + } + else + { + assert update; + addrm.setText("Update"); + addrm.setBackground(new Color(0x1F621F)); + addrm.addActionListener(l -> externalPluginManager.update()); + } + addrm.setBorder(new LineBorder(addrm.getBackground().darker())); + addrm.setFocusPainted(false); + + layout.setHorizontalGroup(layout.createSequentialGroup() + .addComponent(icon, ICON_WIDTH, ICON_WIDTH, ICON_WIDTH) + .addGap(5) + .addGroup(layout.createParallelGroup() + .addGroup(layout.createSequentialGroup() + .addComponent(pluginName, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addComponent(author, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)) + .addComponent(description, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(version, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, 100) + .addComponent(help, 0, 24, 24) + .addComponent(configure, 0, 24, 24) + .addComponent(addrm, 0, 50, GroupLayout.PREFERRED_SIZE) + .addGap(5)))); + + layout.setVerticalGroup(layout.createParallelGroup() + .addComponent(icon, HEIGHT, HEIGHT, HEIGHT) + .addGroup(layout.createSequentialGroup() + .addGap(5) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(pluginName) + .addComponent(author)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addComponent(description) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(version, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(help, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(configure, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT) + .addComponent(addrm, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)) + .addGap(5))); + } + + float setFilter(String[] filter) + { + ToDoubleFunction match = r -> Stream.of(filter) + .mapToDouble(l -> Math.pow(DISTANCE.apply(l, r), 2)) + .max() + .orElse(0.D); + + double sim = SPACES.splitAsStream(manifest.getDisplayName()).collect(Collectors.averagingDouble(match)) * 2; + + if (manifest.getTags() != null) + { + sim += Stream.of(manifest.getTags()).mapToDouble(match).sum(); + } + + return this.filter = (float) sim; + } + } + + private final PluginListPanel pluginListPanel; + private final ExternalPluginManager externalPluginManager; + private final PluginManager pluginManager; + private final ExternalPluginClient externalPluginClient; + private final ScheduledExecutorService executor; + + private final IconTextField searchBar; + private final JLabel refreshing; + private final JPanel mainPanel; + private List plugins = null; + + @Inject + PluginHubPanel( + PluginListPanel pluginListPanel, + ExternalPluginManager externalPluginManager, + PluginManager pluginManager, + ExternalPluginClient externalPluginClient, + ScheduledExecutorService executor) + { + super(false); + this.pluginListPanel = pluginListPanel; + this.externalPluginManager = externalPluginManager; + this.pluginManager = pluginManager; + this.externalPluginClient = externalPluginClient; + this.executor = executor; + + { + Object refresh = "this could just be a lambda, but no, it has to be abstracted"; + getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), refresh); + getActionMap().put(refresh, new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + reloadPluginList(); + } + }); + } + + GroupLayout layout = new GroupLayout(this); + setLayout(layout); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + searchBar = new IconTextField(); + searchBar.setIcon(IconTextField.Icon.SEARCH); + searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR); + searchBar.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR); + searchBar.getDocument().addDocumentListener(new DocumentListener() + { + @Override + public void insertUpdate(DocumentEvent e) + { + filter(); + } + + @Override + public void removeUpdate(DocumentEvent e) + { + filter(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + filter(); + } + }); + + JLabel externalPluginWarning = new JLabel("External plugins are not supported by the RuneLite Developers." + + "They may cause bugs or instability."); + externalPluginWarning.setBackground(new Color(0xFFBB33)); + externalPluginWarning.setForeground(Color.BLACK); + externalPluginWarning.setBorder(new EmptyBorder(5, 5, 5, 2)); + externalPluginWarning.setOpaque(true); + + JLabel externalPluginWarning2 = new JLabel("Use at your own risk!"); + externalPluginWarning2.setHorizontalAlignment(JLabel.CENTER); + externalPluginWarning2.setFont(FontManager.getRunescapeBoldFont()); + externalPluginWarning2.setBackground(externalPluginWarning.getBackground()); + externalPluginWarning2.setForeground(externalPluginWarning.getForeground()); + externalPluginWarning2.setBorder(new EmptyBorder(0, 5, 5, 5)); + externalPluginWarning2.setOpaque(true); + + JButton backButton = new JButton(ConfigPanel.BACK_ICON); + backButton.setRolloverIcon(ConfigPanel.BACK_ICON_HOVER); + SwingUtil.removeButtonDecorations(backButton); + backButton.setToolTipText("Back"); + backButton.addActionListener(l -> pluginListPanel.getMuxer().popState()); + + mainPanel = new JPanel(); + mainPanel.setBorder(BorderFactory.createEmptyBorder(0, 7, 7, 7)); + mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); + mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + refreshing = new JLabel("Loading..."); + refreshing.setHorizontalAlignment(JLabel.CENTER); + + JPanel mainPanelWrapper = new JPanel(); + mainPanelWrapper.setLayout(new BorderLayout()); + mainPanelWrapper.add(mainPanel, BorderLayout.NORTH); + mainPanelWrapper.add(refreshing, BorderLayout.CENTER); + + JScrollPane scrollPane = new JScrollPane(); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setPreferredSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); + scrollPane.getViewport().setLayout(new BorderLayout()); + scrollPane.getViewport().add(mainPanelWrapper, BorderLayout.CENTER); + + layout.setVerticalGroup(layout.createSequentialGroup() + .addComponent(externalPluginWarning) + .addComponent(externalPluginWarning2) + .addGap(10) + .addGroup(layout.createParallelGroup() + .addComponent(backButton) + .addComponent(searchBar)) + .addGap(10) + .addComponent(scrollPane)); + + layout.setHorizontalGroup(layout.createParallelGroup() + .addComponent(externalPluginWarning, 0, Short.MAX_VALUE, Short.MAX_VALUE) + .addComponent(externalPluginWarning2, 0, Short.MAX_VALUE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(backButton) + .addComponent(searchBar) + .addGap(10)) + .addComponent(scrollPane)); + + revalidate(); + + refreshing.setVisible(false); + reloadPluginList(); + } + + private void reloadPluginList() + { + if (refreshing.isVisible()) + { + return; + } + + refreshing.setVisible(true); + mainPanel.removeAll(); + + executor.submit(() -> + { + List manifest; + try + { + manifest = externalPluginClient.downloadManifest(); + } + catch (IOException | VerificationException e) + { + log.error("", e); + SwingUtilities.invokeLater(() -> + { + refreshing.setVisible(false); + mainPanel.add(new JLabel("Downloading the plugin manifest failed")); + + JButton retry = new JButton("Retry"); + retry.addActionListener(l -> reloadPluginList()); + mainPanel.add(retry); + }); + return; + } + + reloadPluginList(manifest); + }); + } + + private void reloadPluginList(List manifest) + { + Map manifests = manifest.stream() + .collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity())); + + Multimap loadedPlugins = HashMultimap.create(); + for (Plugin p : pluginManager.getPlugins()) + { + Class clazz = p.getClass(); + ExternalPluginManifest mf = ExternalPluginManager.getExternalPluginManifest(clazz); + if (mf != null) + { + loadedPlugins.put(mf.getInternalName(), p); + } + } + + Set installed = new HashSet<>(externalPluginManager.getInstalledExternalPlugins()); + + SwingUtilities.invokeLater(() -> + { + plugins = Sets.union(manifests.keySet(), loadedPlugins.keySet()) + .stream() + .map(id -> new PluginItem(manifests.get(id), loadedPlugins.get(id), installed.contains(id))) + .collect(Collectors.toList()); + + refreshing.setVisible(false); + filter(); + }); + } + + void filter() + { + if (refreshing.isVisible()) + { + return; + } + + mainPanel.removeAll(); + + Stream stream = plugins.stream(); + + String search = searchBar.getText(); + boolean isSearching = search != null && !search.trim().isEmpty(); + if (isSearching) + { + String[] searchArray = SPACES.split(search.toLowerCase()); + stream = stream + .filter(p -> p.setFilter(searchArray) > PluginItem.MIN_FILTER_SCORE) + .sorted(Comparator.comparing(PluginItem::getFilter)); + } + else + { + stream = stream + .sorted(Comparator.comparing(PluginItem::isInstalled)); + } + + stream.forEach(mainPanel::add); + mainPanel.revalidate(); + } + + @Override + public void onActivate() + { + revalidate(); + searchBar.setText(""); + reloadPluginList(); + searchBar.requestFocusInWindow(); + } + + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + SwingUtilities.invokeLater(() -> reloadPluginList(ev.getLoadedManifest())); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java index d0a61f0b7d..9b1575acc3 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListItem.java @@ -35,7 +35,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import javax.swing.ImageIcon; @@ -48,6 +47,7 @@ import javax.swing.JToggleButton; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import lombok.Getter; +import net.runelite.client.externalplugins.ExternalPluginManifest; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; import net.runelite.client.util.ImageUtil; @@ -96,6 +96,11 @@ class PluginListItem extends JPanel Collections.addAll(keywords, pluginConfig.getName().toLowerCase().split(" ")); Collections.addAll(keywords, pluginConfig.getDescription().toLowerCase().split(" ")); Collections.addAll(keywords, pluginConfig.getTags()); + ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest(); + if (mf != null) + { + keywords.add(mf.getInternalName()); + } final List popupMenuItems = new ArrayList<>(); @@ -127,6 +132,7 @@ class PluginListItem extends JPanel buttonPanel.setLayout(new GridLayout(1, 2)); add(buttonPanel, BorderLayout.LINE_END); + JMenuItem configMenuItem = null; if (pluginConfig.hasConfigurables()) { JButton configButton = new JButton(CONFIG_ICON); @@ -145,13 +151,18 @@ class PluginListItem extends JPanel configButton.setVisible(true); configButton.setToolTipText("Edit plugin configuration"); - final JMenuItem configMenuItem = new JMenuItem("Configure"); + configMenuItem = new JMenuItem("Configure"); configMenuItem.addActionListener(e -> openGroupConfigPanel()); - popupMenuItems.add(configMenuItem); } - popupMenuItems.add(pluginConfig.createSupportMenuItem()); - addLabelPopupMenu(nameLabel, popupMenuItems); + JMenuItem uninstallItem = null; + if (mf != null) + { + uninstallItem = new JMenuItem("Uninstall"); + uninstallItem.addActionListener(ev -> pluginListPanel.getExternalPluginManager().remove(mf.getInternalName())); + } + + addLabelPopupMenu(nameLabel, configMenuItem, pluginConfig.createSupportMenuItem(), uninstallItem); add(nameLabel, BorderLayout.CENTER); onOffToggle = new PluginToggleButton(); @@ -214,18 +225,6 @@ class PluginListItem extends JPanel pluginListPanel.openConfigurationPanel(pluginConfig); } - /** - * Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and - * adds the passed menu item to a popup menu shown when the label is clicked. - * - * @param label The label to attach the mouseover and click effects to - * @param menuItem The menu item to be shown when the label is clicked - */ - static void addLabelPopupMenu(final JLabel label, final JMenuItem menuItem) - { - addLabelPopupMenu(label, Collections.singletonList(menuItem)); - } - /** * Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and * adds the passed menu items to a popup menu shown when the label is clicked. @@ -233,7 +232,7 @@ class PluginListItem extends JPanel * @param label The label to attach the mouseover and click effects to * @param menuItems The menu items to be shown when the label is clicked */ - static void addLabelPopupMenu(final JLabel label, final Collection menuItems) + static void addLabelPopupMenu(JLabel label, JMenuItem... menuItems) { final JPopupMenu menu = new JPopupMenu(); final Color labelForeground = label.getForeground(); @@ -241,6 +240,11 @@ class PluginListItem extends JPanel for (final JMenuItem menuItem : menuItems) { + if (menuItem == null) + { + continue; + } + // Some machines register mouseEntered through a popup menu, and do not register mouseExited when a popup // menu item is clicked, so reset the label's color when we click one of these options. menuItem.addActionListener(e -> label.setForeground(labelForeground)); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java index abf9ab3bdb..b7b08b780b 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/PluginListPanel.java @@ -37,6 +37,7 @@ import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; +import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; @@ -53,7 +54,9 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ExternalPluginsChanged; import net.runelite.client.events.PluginChanged; +import net.runelite.client.externalplugins.ExternalPluginManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.PluginInstantiationException; @@ -78,8 +81,12 @@ class PluginListPanel extends PluginPanel private final Provider configPanelProvider; private final List fakePlugins = new ArrayList<>(); + @Getter + private final ExternalPluginManager externalPluginManager; + @Getter private final MultiplexingPluginPanel muxer; + private final IconTextField searchBar; private final JScrollPane scrollPane; private final FixedWidthPanel mainPanel; @@ -89,14 +96,17 @@ class PluginListPanel extends PluginPanel public PluginListPanel( ConfigManager configManager, PluginManager pluginManager, + ExternalPluginManager externalPluginManager, ScheduledExecutorService executorService, EventBus eventBus, - Provider configPanelProvider) + Provider configPanelProvider, + Provider pluginHubPanelProvider) { super(false); this.configManager = configManager; this.pluginManager = pluginManager; + this.externalPluginManager = externalPluginManager; this.executorService = executorService; this.configPanelProvider = configPanelProvider; @@ -155,9 +165,15 @@ class PluginListPanel extends PluginPanel mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5)); mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + JButton externalPluginButton = new JButton("Plugin Hub"); + externalPluginButton.setBorder(new EmptyBorder(5, 5, 5, 5)); + externalPluginButton.setLayout(new BorderLayout(0, BORDER_OFFSET)); + externalPluginButton.addActionListener(l -> muxer.pushState(pluginHubPanelProvider.get())); + JPanel northPanel = new FixedWidthPanel(); northPanel.setLayout(new BorderLayout()); northPanel.add(mainPanel, BorderLayout.NORTH); + northPanel.add(externalPluginButton, BorderLayout.SOUTH); scrollPane = new JScrollPane(northPanel); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); @@ -225,6 +241,13 @@ class PluginListPanel extends PluginPanel scrollPane.getVerticalScrollBar().setValue(scrollBarPosition); } + void openWithFilter(String filter) + { + searchBar.setText(filter); + onSearchBarChanged(); + muxer.pushState(this); + } + private void onSearchBarChanged() { final String text = searchBar.getText(); @@ -267,6 +290,18 @@ class PluginListPanel extends PluginPanel } } + void openConfigurationPanel(Plugin plugin) + { + for (PluginListItem pluginListItem : pluginList) + { + if (pluginListItem.getPluginConfig().getPlugin() == plugin) + { + openConfigurationPanel(pluginListItem.getPluginConfig()); + break; + } + } + } + void openConfigurationPanel(PluginConfigurationDescriptor plugin) { ConfigPanel panel = configPanelProvider.get(); @@ -353,4 +388,9 @@ class PluginListPanel extends PluginPanel } } + @Subscribe + private void onExternalPluginsChanged(ExternalPluginsChanged ev) + { + SwingUtilities.invokeLater(this::rebuildPluginList); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java index 10c9f76bc8..58993f63b7 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java @@ -65,8 +65,10 @@ import static net.runelite.client.rs.ClientUpdateCheckMode.NONE; import static net.runelite.client.rs.ClientUpdateCheckMode.VANILLA; import net.runelite.client.ui.FatalErrorDialog; import net.runelite.client.ui.SplashScreen; +import net.runelite.client.util.CountingInputStream; import net.runelite.http.api.RuneLiteAPI; import net.runelite.http.api.worlds.World; +import net.runelite.client.util.VerificationException; import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; diff --git a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java index 34517868d8..131bbb7830 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java @@ -53,7 +53,7 @@ import javax.swing.border.EmptyBorder; import lombok.extern.slf4j.Slf4j; import net.runelite.client.RuneLite; import net.runelite.client.RuneLiteProperties; -import net.runelite.client.rs.VerificationException; +import net.runelite.client.util.VerificationException; import net.runelite.client.util.LinkBrowser; @Slf4j diff --git a/runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java b/runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java similarity index 93% rename from runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java rename to runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java index 5f44362b1a..8543d65295 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/CountingInputStream.java +++ b/runelite-client/src/main/java/net/runelite/client/util/CountingInputStream.java @@ -22,18 +22,18 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package net.runelite.client.rs; +package net.runelite.client.util; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.function.IntConsumer; -class CountingInputStream extends FilterInputStream +public class CountingInputStream extends FilterInputStream { private final IntConsumer changed; - CountingInputStream(InputStream in, IntConsumer changed) + public CountingInputStream(InputStream in, IntConsumer changed) { super(in); this.changed = changed; diff --git a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java b/runelite-client/src/main/java/net/runelite/client/util/VerificationException.java similarity index 97% rename from runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java rename to runelite-client/src/main/java/net/runelite/client/util/VerificationException.java index 4138a12fd3..2f6f1f5dee 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java +++ b/runelite-client/src/main/java/net/runelite/client/util/VerificationException.java @@ -22,7 +22,7 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package net.runelite.client.rs; +package net.runelite.client.util; public class VerificationException extends Exception { diff --git a/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt b/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt new file mode 100644 index 0000000000..2ba1550b51 --- /dev/null +++ b/runelite-client/src/main/resources/net/runelite/client/externalplugins/externalplugins.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDDCCAfSgAwIBAgIJAK8uBanmNQZaMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV +BAMMEHJ1bmVsaXRlLXBsdWdpbnMwHhcNMTkxMjEyMjEwNzUxWhcNMjUxMjEwMjEw +NzUxWjAbMRkwFwYDVQQDDBBydW5lbGl0ZS1wbHVnaW5zMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEApu11OVANSU+pHaXRxB7fIZapucJ6BT46neicEixs +NVPuK/QRVjO/G8F++MXFD/tlZUOEDllDN8uaHBIVwxilqEVYL7oX65Esl7qqC1TZ +WGdjiMyYoK3CXWEWB4w+CdB31T7JG2HqH45ZsVs+U9OVWBkNkL5nNQNPOmZFd+3A +yCb9nGlO7SxduiHpwh3CV19jY47y8tevyo5qpaBuQeWtu3vbpeer0kbDarwD3xoF +yUMPRK518gxRUSmOpsSG5viQ731mKVCUUfIXz91d3s+kJYAjORHS4zJe9s+1dljp +oLYNLkaP6m3CmNtC84OxkmognvZTNMbiQ3GQm/BK4sdjPQIDAQABo1MwUTAdBgNV +HQ4EFgQUxrkiRXNd0OHPMkqgl9UgV1//OuQwHwYDVR0jBBgwFoAUxrkiRXNd0OHP +Mkqgl9UgV1//OuQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA +StPyblz3aqOM5z2KqHX1B7Z3Q8B58g55YSefpcfwWEc6LT4HCztszcZDteWpV3W2 +ERfemkGKgsDhQ0qkzIt7tS5eNN3PPj7RZZm7vl5HquQ1vC/33ri/Z3CEKzbW7knt +i1iEpx8E9DKb9J9DjdKwNxSomOyCOFUt9YoQJs80xc1mwPDd6aWR3xwvnEUimkm+ +Dbj7HMOXLeyN810wkeWcT8nC5GhxH3ZAmVExBHsaIOB876RntzshBehjY8s8JQhw +R+fT1e8EhYMM9ylYDk1KIWFWrAujjU04lS9tXZ5C2e7fr9R953XN6Y0PNM/taNTU +GzwGroJZI02V+1ADO14rRA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png b/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_configure.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf5a192e56e9ff7870ea5e09fbd9ed83e127f2a GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf&@~GAIf1 z3ua(sVrF4w=j7uT5R{No*U&fji;hpq?&zO1b@kd!yY?M8c=*)CtGDjld-3}1`>#L$ z8v6dM1L|$_ba4!kkn}ysF4Sbe;CfI#+2QED>dvkk_y78*8}+zopr0As?#R{#J2 literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png b/runelite-client/src/main/resources/net/runelite/client/plugins/config/pluginhub_help.png new file mode 100644 index 0000000000000000000000000000000000000000..ffe3b6084021e9b5702f245a1524caa1152db101 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbMf_5hy{S0KG~2!dBDI_3eLZc`HE z7tFxO#=*nOCoUnQqNc8A>g(qp7#0~9pH^C4-_Y6JH*wOG*>mSDTd{imhV47|>^pw` z>h+tq?%aF!;{ErZf4NU?V*#2o)6>ND!zC z_2T7i?&%)04nKYVJNM!OF~7B;?kW#G_sY#%b4Byw(tS@{?&ixnMBh~}i7HqSdTi&e zm-l{fIqF9IPM_!;Joj_>r{nD<{K|{9nmYse+1!ph yRD`WR^~rJW#1`Z1(35_4yZ-;$VV(0~zHU;md{@Me*CjwlGI+ZBxvX zWm9uU|HLV?W-nd4e*3O{M~|O4cmCq#tJiNmd-3x1$L~LX|9#LmPnUs#F~ZZuF~sBe z+AA-E4jG8BKIrC(aJ}Pjr1l`Ixrlg($Q{SriQB&aoqtqhw_5Hj>&HJQ&$PZjv+U9m zhEHo={s?9KYI4ET@#g$qo48fKF5CC#y4F9cx)c2Q_`G`g;7{)J{-5xQJs11BYhPl< z;;$Dz@11i;nQzXq#$D;Xnm3kLT+681C;#o%+yP)^Ykkt+BJ^DM?EI}im~O{joc`zLj^57RuJ_{2?dz1(Ol$Jn zzJHOKEcH=t*Nh7h+mdY~B<|n%`nTqbM55Q3oh!~S`SDPvJ|K5dyZB1|uJDIZVK=&V zT1#&_Hhm0Wt2blX!$C^zrXCOCcpT~bk^SAYfoTS(Vukb zlzFvsZ?(QSzrL<^$;NwG)c)Gnd&~69mNSafM6Hm6!Fdr@EdROP`&?s%w>*gh#vy~J LtDnm{r-UW|