diff --git a/buildSrc/src/main/kotlin/BootstrapTask.kt b/buildSrc/src/main/kotlin/BootstrapTask.kt index 48274fd647..6673b4335c 100644 --- a/buildSrc/src/main/kotlin/BootstrapTask.kt +++ b/buildSrc/src/main/kotlin/BootstrapTask.kt @@ -34,6 +34,7 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT private fun getArtifacts(): Array { val artifacts = ArrayList() + val artifactsSet = HashSet() project.configurations["runtimeClasspath"].resolvedConfiguration.resolvedArtifacts.forEach { val module = it.moduleVersion.id.toString() @@ -44,7 +45,11 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT val version = splat[2] lateinit var path: String - if (it.file.name.contains(ProjectVersions.openosrsVersion)) { + if (it.file.name.contains("injected-client") || + it.file.name.contains("runelite-client") || + it.file.name.contains("http-api") || + it.file.name.contains("runescape-api") || + it.file.name.contains("runelite-api")) { path = "https://github.com/open-osrs/hosting/raw/master/${type}/${it.file.name}" } else if (it.file.name.contains("injection-annotations") || it.file.name.contains("rxrelay")) { path = "https://github.com/open-osrs/hosting/raw/master/" + group.replace(".", "/") + "/${name}/$version/${it.file.name}" @@ -62,14 +67,18 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT path += "${name}/$version/${name}-$version.jar" } - val artifactFile = File(it.file.absolutePath) + val filePath = it.file.absolutePath + val artifactFile = File(filePath) - artifacts.add(JsonBuilder( - "name" to it.file.name, - "path" to path, - "size" to artifactFile.length(), - "hash" to hash(artifactFile.readBytes()) - )) + if (!artifactsSet.contains(filePath)) { + artifactsSet.add(filePath) + artifacts.add(JsonBuilder( + "name" to it.file.name, + "path" to path, + "size" to artifactFile.length(), + "hash" to hash(artifactFile.readBytes()) + )) + } } artifacts.add(JsonBuilder( diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index d9997072d5..908c80eeec 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -62,6 +62,7 @@ object Libraries { const val guice = "4.2.2" const val h2 = "1.4.200" const val hamcrest = "2.2" + const val javagroups = "4.0.0.Final" const val javax = "1.3.2" const val javaxInject = "1" const val jna = "5.5.0" @@ -108,6 +109,7 @@ object Libraries { const val guiceTestlib = "com.google.inject.extensions:guice-testlib:${Versions.guice}" const val h2 = "com.h2database:h2:${Versions.h2}" const val hamcrest = "org.hamcrest:hamcrest-library:${Versions.hamcrest}" + const val javagroups = "org.jgroups:jgroups:${Versions.javagroups}" const val javax = "javax.annotation:javax.annotation-api:${Versions.javax}" const val javaxInject = "javax.inject:javax.inject:${Versions.javaxInject}" const val jna = "net.java.dev.jna:jna:${Versions.jna}" diff --git a/runelite-client/runelite-client.gradle.kts b/runelite-client/runelite-client.gradle.kts index 1487c30591..20e51207a5 100644 --- a/runelite-client/runelite-client.gradle.kts +++ b/runelite-client/runelite-client.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(Libraries.okhttp3) implementation(Libraries.rxjava) implementation(Libraries.jna) + implementation(Libraries.javagroups) implementation(Libraries.jnaPlatform) implementation(Libraries.discord) implementation(Libraries.substance) @@ -134,11 +135,15 @@ tasks { inputs.properties(tokens) from("src/main/resources") { - include("open.osrs.properties") include("sentry.properties") } into("${buildDir}/resources/main") + from("src/main/resources/net/runelite/client") { + include("open.osrs.properties") + } + into("${buildDir}/resources/main/net/runelite/client") + filter(ReplaceTokens::class, "tokens" to tokens) filteringCharset = "UTF-8" } 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 ef0fd88cc6..47f5e63ba8 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -40,6 +40,7 @@ import java.net.PasswordAuthentication; import java.nio.file.Paths; import java.util.Locale; import java.util.Optional; +import java.util.UUID; import javax.annotation.Nullable; import javax.inject.Provider; import javax.inject.Singleton; @@ -88,7 +89,7 @@ import net.runelite.client.ui.overlay.arrow.ArrowWorldOverlay; import net.runelite.client.ui.overlay.infobox.InfoBoxOverlay; import net.runelite.client.ui.overlay.tooltip.TooltipOverlay; import net.runelite.client.ui.overlay.worldmap.WorldMapOverlay; -import net.runelite.client.util.AppLock; +import net.runelite.client.util.Groups; import net.runelite.client.util.WorldUtil; import net.runelite.client.ws.PartyService; import net.runelite.http.api.worlds.World; @@ -112,6 +113,7 @@ public class RuneLite public static final File DEFAULT_CONFIG_FILE = new File(RUNELITE_DIR, "runeliteplus.properties"); public static final Locale SYSTEM_LOCALE = Locale.getDefault(); public static boolean allowPrivateServer = false; + public static String uuid = UUID.randomUUID().toString(); @Getter private static Injector injector; @@ -191,6 +193,9 @@ public class RuneLite @Inject private Provider partyService; + @Inject + private Groups groups; + @Inject private Hooks hooks; @@ -210,9 +215,6 @@ public class RuneLite @Inject private Scheduler scheduler; - @Inject - private AppLock appLock; - public static void main(String[] args) throws Exception { Locale.setDefault(Locale.ENGLISH); @@ -383,15 +385,12 @@ public class RuneLite // Tell the plugin manager if client is outdated or not pluginManager.setOutdated(isOutdated); - // Load external plugins + // Load external plugin manager externalPluginManager.startExternalUpdateManager(); externalPluginManager.startExternalPluginManager(); - if (appLock.lock(this.getClass().getName())) - { - RuneLiteSplashScreen.stage(.59, "Updating external plugins"); - externalPluginManager.update(); - } + // Update external plugins + externalPluginManager.update(); // Load the plugins, but does not start them yet. // This will initialize configuration @@ -510,7 +509,7 @@ public class RuneLite { clientSessionManager.shutdown(); discordService.close(); - appLock.release(); + groups.close(); } private static class ConfigFileConverter implements ValueConverter 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 0a230af91d..3d6ff691b9 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -52,7 +52,7 @@ public class RuneLiteProperties static { - try (InputStream in = RuneLiteProperties.class.getResourceAsStream("/open.osrs.properties")) + try (InputStream in = RuneLiteProperties.class.getResourceAsStream("open.osrs.properties")) { properties.load(in); } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index c8857739b9..0e441b547f 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -70,7 +70,10 @@ import net.runelite.client.events.ConfigChanged; import net.runelite.client.plugins.ExternalPluginManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.util.ColorUtil; +import net.runelite.client.util.Groups; import org.apache.commons.lang3.StringUtils; +import org.jgroups.Message; +import org.jgroups.util.Util; @Singleton @Slf4j @@ -82,15 +85,24 @@ public class ConfigManager private final Properties properties = new Properties(); private final Map pendingChanges = new HashMap<>(); private final File settingsFileInput; + private final Groups groups; @Inject EventBus eventBus; @Inject - public ConfigManager(@Named("config") File config, ScheduledExecutorService scheduledExecutorService) + public ConfigManager( + @Named("config") File config, + ScheduledExecutorService scheduledExecutorService, + Groups groups) { this.settingsFileInput = config; + this.groups = groups; + scheduledExecutorService.scheduleWithFixedDelay(this::sendConfig, 30, 30, TimeUnit.SECONDS); + + groups.getMessageObjectSubject() + .subscribe(this::receive); } @SuppressWarnings("unchecked") @@ -450,6 +462,7 @@ public class ConfigManager configChanged.setKey(key); configChanged.setOldValue(null); configChanged.setNewValue(value); + eventBus.post(ConfigChanged.class, configChanged); }); } @@ -525,7 +538,17 @@ public class ConfigManager return null; } + public void setConfiguration(String groupName, String key, Object value) + { + setConfiguration(groupName, key, objectToString(value)); + } + public void setConfiguration(String groupName, String key, String value) + { + setConfiguration(groupName, key, value, null); + } + + public void setConfiguration(String groupName, String key, String value, String origin) { String oldValue = (String) properties.setProperty(groupName + "." + key, value); @@ -547,16 +570,23 @@ public class ConfigManager configChanged.setKey(key); configChanged.setOldValue(oldValue); configChanged.setNewValue(value); + configChanged.setOrigin(origin == null ? RuneLite.uuid : origin); + configChanged.setPath(settingsFileInput.getAbsolutePath()); eventBus.post(ConfigChanged.class, configChanged); - } - public void setConfiguration(String groupName, String key, Object value) - { - setConfiguration(groupName, key, objectToString(value)); + if (origin == null) + { + broadcast(configChanged); + } } public void unsetConfiguration(String groupName, String key) + { + unsetConfiguration(groupName, key, null); + } + + public void unsetConfiguration(String groupName, String key, String origin) { String oldValue = (String) properties.remove(groupName + "." + key); @@ -577,8 +607,15 @@ public class ConfigManager configChanged.setGroup(groupName); configChanged.setKey(key); configChanged.setOldValue(oldValue); + configChanged.setOrigin(origin == null ? RuneLite.uuid : origin); + configChanged.setPath(settingsFileInput.getAbsolutePath()); eventBus.post(ConfigChanged.class, configChanged); + + if (origin == null) + { + broadcast(configChanged); + } } public ConfigDescriptor getConfigDescriptor(Object configurationProxy) @@ -842,4 +879,40 @@ public class ConfigManager syncPropertiesFromFile(newestFile); } + + private void broadcast(ConfigChanged configChanged) + { + groups.sendConfig(null, configChanged); + } + + public void receive(Message message) + { + if (message.getObject() instanceof String) + { + return; + } + + try + { + ConfigChanged configChanged = Util.objectFromByteBuffer(message.getBuffer()); + + if (!configChanged.getPath().equals(settingsFileInput.getAbsolutePath())) + { + return; + } + + if (configChanged.getNewValue() == null) + { + unsetConfiguration(configChanged.getGroup(), configChanged.getKey(), configChanged.getOrigin()); + } + else + { + setConfiguration(configChanged.getGroup(), configChanged.getKey(), configChanged.getNewValue(), configChanged.getOrigin()); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java b/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java index d36dc7dd9a..39010761ee 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/OpenOSRSConfig.java @@ -342,11 +342,23 @@ public interface OpenOSRSConfig extends Config return new Title(); } + @ConfigItem( + keyName = "localSync", + name = "Sync local instances", + description = "Enables multiple local instances of OpenOSRS to communicate (this enables syncing plugin state and config options)", + position = 21, + titleSection = "miscTitle" + ) + default boolean localSync() + { + return true; + } + @ConfigItem( keyName = "keyboardPin", name = "Keyboard bank pin", description = "Enables you to type your bank pin", - position = 21, + position = 22, titleSection = "miscTitle" ) default boolean keyboardPin() @@ -358,7 +370,7 @@ public interface OpenOSRSConfig extends Config keyName = "detachHotkey", name = "Detach Cam", description = "Detach Camera hotkey, press this and it will activate detatched camera.", - position = 22, + position = 23, titleSection = "miscTitle" ) default Keybind detachHotkey() diff --git a/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java b/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java index dd4e0fe7ef..d8f3bee83c 100644 --- a/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java +++ b/runelite-client/src/main/java/net/runelite/client/events/ConfigChanged.java @@ -24,6 +24,7 @@ */ package net.runelite.client.events; +import java.io.Serializable; import lombok.Data; import net.runelite.api.events.Event; @@ -31,7 +32,7 @@ import net.runelite.api.events.Event; * An event where a configuration entry has been modified. */ @Data -public class ConfigChanged implements Event +public class ConfigChanged implements Event, Serializable { /** * The parent group for the key. @@ -52,4 +53,12 @@ public class ConfigChanged implements Event * The new value of the entry, null if the entry has been unset. */ private String newValue; + /** + * The client where the config value was changed from + */ + private String origin; + /** + * Path of the current config file + */ + private String path; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java index 4fd7bc5d77..fc77b8d85a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java @@ -1,13 +1,36 @@ +/* + * Copyright (c) 2020, Owain van Brakel + * 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; import com.google.common.collect.ImmutableList; -import com.google.common.graph.GraphBuilder; -import com.google.common.graph.MutableGraph; import com.google.inject.Binder; import com.google.inject.CreationException; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -30,13 +53,13 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Singleton; import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; import lombok.AccessLevel; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -48,12 +71,16 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.OpenOSRSConfig; import net.runelite.client.eventbus.EventBus; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.ExternalPluginChanged; import net.runelite.client.events.ExternalRepositoryChanged; import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.RuneLiteSplashScreen; +import net.runelite.client.util.Groups; import net.runelite.client.util.MiscUtils; import net.runelite.client.util.SwingUtil; +import org.jgroups.Message; +import org.jgroups.ReceiverAdapter; import org.pf4j.DefaultPluginManager; import org.pf4j.DependencyResolver; import org.pf4j.JarPluginLoader; @@ -65,6 +92,8 @@ import org.pf4j.PluginDescriptorFinder; import org.pf4j.PluginLoader; import org.pf4j.PluginRepository; import org.pf4j.PluginRuntimeException; +import org.pf4j.PluginState; +import org.pf4j.PluginStateEvent; import org.pf4j.PluginWrapper; import org.pf4j.RuntimeMode; import org.pf4j.update.DefaultUpdateRepository; @@ -75,8 +104,7 @@ import org.pf4j.update.VerifyException; @Slf4j @Singleton -public -class ExternalPluginManager +public class ExternalPluginManager extends ReceiverAdapter { public static ArrayList pluginClassLoaders = new ArrayList<>(); private final PluginManager runelitePluginManager; @@ -86,35 +114,41 @@ class ExternalPluginManager private final OpenOSRSConfig openOSRSConfig; private final EventBus eventBus; private final ConfigManager configManager; - private final List plugins = new CopyOnWriteArrayList<>(); private final Map pluginsMap = new HashMap<>(); @Getter(AccessLevel.PUBLIC) + private final Map> pluginsInfoMap = new HashMap<>(); + private final Groups groups; + @Getter(AccessLevel.PUBLIC) private UpdateManager updateManager; + private Map lastPluginRelease = new HashMap<>(); @Inject public ExternalPluginManager( PluginManager pluginManager, OpenOSRSConfig openOSRSConfig, EventBus eventBus, - ConfigManager configManager) + ConfigManager configManager, + Groups groups) { this.runelitePluginManager = pluginManager; this.openOSRSConfig = openOSRSConfig; this.eventBus = eventBus; this.configManager = configManager; + this.groups = groups; //noinspection ResultOfMethodCallIgnored EXTERNALPLUGIN_DIR.mkdirs(); initPluginManager(); + + groups.getMessageStringSubject() + .subscribe(this::receive); } private void initPluginManager() { - boolean debug = RuneLiteProperties.getLauncherVersion() == null && RuneLiteProperties.getPluginPath() != null; - - this.externalPluginManager = new DefaultPluginManager( - debug ? Paths.get(RuneLiteProperties.getPluginPath() + File.separator + "release") + externalPluginManager = new DefaultPluginManager( + RuneLiteProperties.getPluginPath() != null ? Paths.get(RuneLiteProperties.getPluginPath()) : EXTERNALPLUGIN_DIR.toPath()) { @Override @@ -158,7 +192,70 @@ class ExternalPluginManager @Override public RuntimeMode getRuntimeMode() { - return debug ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; + return RuneLiteProperties.getLauncherVersion() == null ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT; + } + + @Override + protected void resolvePlugins() + { + // retrieves the plugins descriptors + List descriptors = new ArrayList<>(); + for (PluginWrapper plugin : plugins.values()) + { + descriptors.add(plugin.getDescriptor()); + } + + // retrieves the plugins descriptors from the resolvedPlugins list. This allows to load plugins that have already loaded dependencies. + for (PluginWrapper plugin : resolvedPlugins) + { + descriptors.add(plugin.getDescriptor()); + } + + DependencyResolver.Result result = dependencyResolver.resolve(descriptors); + + if (result.hasCyclicDependency()) + { + throw new DependencyResolver.CyclicDependencyException(); + } + + List notFoundDependencies = result.getNotFoundDependencies(); + if (!notFoundDependencies.isEmpty()) + { + throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies); + } + + List wrongVersionDependencies = result.getWrongVersionDependencies(); + if (!wrongVersionDependencies.isEmpty()) + { + throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies); + } + + List sortedPlugins = result.getSortedPlugins(); + + // move plugins from "unresolved" to "resolved" + for (String pluginId : sortedPlugins) + { + PluginWrapper pluginWrapper = plugins.get(pluginId); + + //The plugin is already resolved. Don't put a copy in the resolvedPlugins. + if (resolvedPlugins.contains(pluginWrapper)) + { + continue; + } + + if (unresolvedPlugins.remove(pluginWrapper)) + { + PluginState pluginState = pluginWrapper.getPluginState(); + if (pluginState != PluginState.DISABLED) + { + pluginWrapper.setPluginState(PluginState.RESOLVED); + } + + resolvedPlugins.add(pluginWrapper); + + firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); + } + } } @Override @@ -209,8 +306,129 @@ class ExternalPluginManager log.error(e.getMessage(), e); } } + + @Override + public PluginState stopPlugin(String pluginId) + { + if (!plugins.containsKey(pluginId)) + { + throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId)); + } + + PluginWrapper pluginWrapper = getPlugin(pluginId); + org.pf4j.PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor(); + PluginState pluginState = pluginWrapper.getPluginState(); + if (PluginState.STOPPED == pluginState) + { + log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor)); + return PluginState.STOPPED; + } + + // test for disabled plugin + if (PluginState.DISABLED == pluginState) + { + // do nothing + return pluginState; + } + + pluginWrapper.getPlugin().stop(); + pluginWrapper.setPluginState(PluginState.STOPPED); + startedPlugins.remove(pluginWrapper); + + firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); + + return pluginWrapper.getPluginState(); + } + + @Override + public boolean unloadPlugin(String pluginId) + { + try + { + PluginState pluginState = stopPlugin(pluginId); + if (PluginState.STARTED == pluginState) + { + return false; + } + + PluginWrapper pluginWrapper = getPlugin(pluginId); + + // remove the plugin + plugins.remove(pluginId); + getResolvedPlugins().remove(pluginWrapper); + + firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState)); + + // remove the classloader + Map pluginClassLoaders = getPluginClassLoaders(); + if (pluginClassLoaders.containsKey(pluginId)) + { + ClassLoader classLoader = pluginClassLoaders.remove(pluginId); + if (classLoader instanceof Closeable) + { + try + { + ((Closeable) classLoader).close(); + } + catch (IOException e) + { + throw new PluginRuntimeException(e, "Cannot close classloader"); + } + } + } + + return true; + } + catch (IllegalArgumentException e) + { + // ignore not found exceptions because this method is recursive + } + + return false; + } + + @Override + public boolean deletePlugin(String pluginId) + { + if (!plugins.containsKey(pluginId)) + { + throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId)); + } + + PluginWrapper pluginWrapper = getPlugin(pluginId); + // stop the plugin if it's started + PluginState pluginState = stopPlugin(pluginId); + if (PluginState.STARTED == pluginState) + { + log.error("Failed to stop plugin '{}' on delete", pluginId); + return false; + } + + // get an instance of plugin before the plugin is unloaded + // for reason see https://github.com/pf4j/pf4j/issues/309 + + org.pf4j.Plugin plugin = pluginWrapper.getPlugin(); + + if (!unloadPlugin(pluginId)) + { + log.error("Failed to unload plugin '{}' on delete", pluginId); + return false; + } + + // notify the plugin as it's deleted + plugin.delete(); + + Path pluginPath = pluginWrapper.getPluginPath(); + + return pluginRepository.deletePluginPath(pluginPath); + } }; - this.externalPluginManager.setSystemVersion(SYSTEM_VERSION); + externalPluginManager.setSystemVersion(SYSTEM_VERSION); + } + + public boolean developmentMode() + { + return externalPluginManager.isDevelopment(); } public boolean doesGhRepoExist(String owner, String name) @@ -263,7 +481,7 @@ class ExternalPluginManager { try { - this.externalPluginManager.loadPlugins(); + externalPluginManager.loadPlugins(); } catch (Exception ex) { @@ -275,7 +493,7 @@ class ExternalPluginManager for (String dep : deps) { - install(dep); + updateManager.installPlugin(dep, null); } startExternalPluginManager(); @@ -292,7 +510,7 @@ class ExternalPluginManager loadOldFormat(); } - this.updateManager = new UpdateManager(this.externalPluginManager, repositories); + updateManager = new UpdateManager(externalPluginManager, repositories); saveConfig(); } @@ -361,7 +579,7 @@ class ExternalPluginManager openOSRSConfig.setExternalRepositories("OpenOSRS:https://raw.githubusercontent.com/open-osrs/plugin-hosting/master/"); } - this.updateManager = new UpdateManager(this.externalPluginManager, repositories); + updateManager = new UpdateManager(externalPluginManager, repositories); } public void addGHRepository(String owner, String name) @@ -407,12 +625,9 @@ class ExternalPluginManager openOSRSConfig.setExternalRepositories(config.toString()); } - private List scanAndInstantiate(List plugins, boolean init, boolean initConfig) throws IOException + private void scanAndInstantiate(List plugins, boolean init, boolean initConfig) { RuneLiteSplashScreen.stage(.66, "Loading external plugins"); - MutableGraph> graph = GraphBuilder - .directed() - .build(); AtomicInteger loaded = new AtomicInteger(); List scannedPlugins = new CopyOnWriteArrayList<>(); @@ -451,6 +666,7 @@ class ExternalPluginManager Plugin plugininst; try { + //noinspection unchecked plugininst = instantiate(scannedPlugins, (Class) plugin.getClass(), init, initConfig); scannedPlugins.add(plugininst); } @@ -477,7 +693,6 @@ class ExternalPluginManager }); } - return scannedPlugins; } @SuppressWarnings("unchecked") @@ -492,7 +707,7 @@ class ExternalPluginManager Optional dependency = Stream.concat(runelitePluginManager.getPlugins().stream(), scannedPlugins.stream()) .filter(p -> p.getClass() == pluginDependency.value()).findFirst(); - if (!dependency.isPresent()) + if (dependency.isEmpty()) { throw new PluginInstantiationException( "Unmet dependency for " + clazz.getSimpleName() + ": " + pluginDependency.value().getSimpleName()); @@ -551,12 +766,12 @@ class ExternalPluginManager { try { - SwingUtilities.invokeAndWait(() -> + SwingUtil.syncExec(() -> { try { - runelitePluginManager.startPlugin(plugin); runelitePluginManager.add(plugin); + runelitePluginManager.startPlugin(plugin); eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginsMap.get(plugin.getClass().getSimpleName()), plugin, true)); @@ -586,43 +801,53 @@ class ExternalPluginManager return plugin; } + private void checkDepsAndStart(List startedPlugins, List scannedPlugins, PluginWrapper pluginWrapper) + { + boolean depsLoaded = true; + for (PluginDependency dependency : pluginWrapper.getDescriptor().getDependencies()) + { + if (startedPlugins.stream().noneMatch(pl -> pl.getPluginId().equals(dependency.getPluginId()))) + { + depsLoaded = false; + } + } + + if (!depsLoaded) + { + // This should never happen but can crash the client + return; + } + + scannedPlugins.addAll(loadPlugin(pluginWrapper.getPluginId())); + } + public void loadPlugins() { - this.externalPluginManager.startPlugins(); + externalPluginManager.startPlugins(); List startedPlugins = getStartedPlugins(); List scannedPlugins = new ArrayList<>(); for (PluginWrapper plugin : startedPlugins) { - boolean depsLoaded = true; - for (PluginDependency dependency : plugin.getDescriptor().getDependencies()) - { - if (startedPlugins.stream().noneMatch(pl -> pl.getPluginId().equals(dependency.getPluginId()))) - { - depsLoaded = false; - } - } - - if (!depsLoaded) - { - // This should never happen but can crash the client - continue; - } - - scannedPlugins.addAll(loadPlugin(plugin.getPluginId())); + checkDepsAndStart(startedPlugins, scannedPlugins, plugin); } - startPlugins(scannedPlugins, false, false); - } + scanAndInstantiate(scannedPlugins, false, false); - private void startPlugins(List scannedPlugins, boolean init, boolean initConfig) - { - try + if (groups.getInstanceCount() > 1) { - plugins.addAll(scanAndInstantiate(scannedPlugins, init, initConfig)); + for (String pluginId : getDisabledPlugins()) + { + groups.sendString("STOPEXTERNAL;" + pluginId); + } } - catch (IOException ignored) + else { + for (String pluginId : getDisabledPlugins()) + { + externalPluginManager.enablePlugin(pluginId); + externalPluginManager.deletePlugin(pluginId); + } } } @@ -633,55 +858,57 @@ class ExternalPluginManager for (Plugin plugin : extensions) { pluginClassLoaders.add(plugin.getClass().getClassLoader()); + + pluginsMap.remove(plugin.getClass().getSimpleName()); pluginsMap.put(plugin.getClass().getSimpleName(), pluginId); + + pluginsInfoMap.remove(plugin.getClass().getSimpleName()); + + AtomicReference support = new AtomicReference<>(""); + AtomicReference version = new AtomicReference<>(""); + + updateManager.getRepositories().forEach(repository -> + repository.getPlugins().forEach((key, value) -> { + if (key.equals(pluginId)) + { + support.set(value.projectUrl); + + for (PluginInfo.PluginRelease release : value.releases) + { + if (externalPluginManager.getSystemVersion().equals("0.0.0") || externalPluginManager.getVersionManager().checkVersionConstraint(externalPluginManager.getSystemVersion(), release.requires)) + { + if (lastPluginRelease.get(pluginId) == null) + { + lastPluginRelease.put(pluginId, release); + } + else if (externalPluginManager.getVersionManager().compareVersions(release.version, lastPluginRelease.get(pluginId).version) > 0) + { + lastPluginRelease.put(pluginId, release); + } + } + } + + version.set(lastPluginRelease.get(pluginId).version); + } + })); + + pluginsInfoMap.put( + plugin.getClass().getSimpleName(), + new HashMap<>() + {{ + put("version", version.get()); + put("id", externalPluginManager.getPlugin(pluginId).getDescriptor().getPluginId()); + put("provider", externalPluginManager.getPlugin(pluginId).getDescriptor().getProvider()); + put("support", support.get()); + }} + ); + scannedPlugins.add(plugin); } return scannedPlugins; } - private void stopPlugins() - { - List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); - - for (PluginWrapper pluginWrapper : startedPlugins) - { - String pluginId = pluginWrapper.getDescriptor().getPluginId(); - List extensions = externalPluginManager.getExtensions(Plugin.class, pluginId); - - for (Plugin plugin : runelitePluginManager.getPlugins()) - { - if (!extensions.get(0).getClass().getName().equals(plugin.getClass().getName())) - { - continue; - } - - try - { - SwingUtilities.invokeAndWait(() -> - { - try - { - runelitePluginManager.stopPlugin(plugin); - } - catch (Exception e2) - { - throw new RuntimeException(e2); - } - }); - runelitePluginManager.remove(plugin); - - eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false)); - } - catch (Exception ex) - { - log.warn("unable to stop plugin", ex); - return; - } - } - } - } - private Path stopPlugin(String pluginId) { List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); @@ -704,7 +931,7 @@ class ExternalPluginManager try { - SwingUtilities.invokeAndWait(() -> + SwingUtil.syncExec(() -> { try { @@ -716,6 +943,7 @@ class ExternalPluginManager } }); runelitePluginManager.remove(plugin); + pluginClassLoaders.remove(plugin.getClass().getClassLoader()); eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false)); @@ -734,13 +962,13 @@ class ExternalPluginManager public boolean install(String pluginId) throws VerifyException { - if (getDisabledPlugins().contains(pluginId)) { - this.externalPluginManager.enablePlugin(pluginId); - this.externalPluginManager.startPlugin(pluginId); + externalPluginManager.enablePlugin(pluginId); + externalPluginManager.startPlugin(pluginId); - startPlugins(loadPlugin(pluginId), true, false); + groups.broadcastSring("STARTEXTERNAL;" + pluginId); + scanAndInstantiate(loadPlugin(pluginId), true, false); return true; } @@ -775,7 +1003,9 @@ class ExternalPluginManager updateManager.installPlugin(pluginId, null); - startPlugins(loadPlugin(pluginId), true, true); + scanAndInstantiate(loadPlugin(pluginId), true, true); + + groups.broadcastSring("STARTEXTERNAL;" + pluginId); } catch (DependencyResolver.DependenciesNotFoundException ex) { @@ -792,6 +1022,11 @@ class ExternalPluginManager } public boolean uninstall(String pluginId) + { + return uninstall(pluginId, false); + } + + public boolean uninstall(String pluginId, boolean skip) { Path pluginPath = stopPlugin(pluginId); @@ -801,13 +1036,35 @@ class ExternalPluginManager } externalPluginManager.stopPlugin(pluginId); - externalPluginManager.disablePlugin(pluginId); + + if (skip) + { + return true; + } + + if (groups.getInstanceCount() > 1) + { + groups.sendString("STOPEXTERNAL;" + pluginId); + } + else + { + externalPluginManager.deletePlugin(pluginId); + } return true; } public void update() { + if (groups.getInstanceCount() > 1) + { + // Do not update when there is more than one client open -> api might contain changes + log.info("Not updating external plugins since there is more than 1 client open"); + return; + } + + RuneLiteSplashScreen.stage(.59, "Updating external plugins"); + boolean error = false; if (updateManager.hasUpdates()) { @@ -828,6 +1085,7 @@ class ExternalPluginManager } catch (PluginRuntimeException ex) { + // This should never happen but can crash the client log.warn("Cannot update plugin '{}', the user probably has another client open", plugin.id); error = true; break; @@ -861,15 +1119,99 @@ class ExternalPluginManager public List getDisabledPlugins() { - return this.externalPluginManager.getResolvedPlugins() + return externalPluginManager.getResolvedPlugins() .stream() - .filter(not(this.externalPluginManager.getStartedPlugins()::contains)) + .filter(not(externalPluginManager.getStartedPlugins()::contains)) .map(PluginWrapper::getPluginId) .collect(Collectors.toList()); } public List getStartedPlugins() { - return this.externalPluginManager.getStartedPlugins(); + return externalPluginManager.getStartedPlugins(); + } + + public Boolean reloadStart(String pluginId) + { + externalPluginManager.loadPlugins(); + externalPluginManager.startPlugin(pluginId); + + List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); + List scannedPlugins = new ArrayList<>(); + + for (PluginWrapper pluginWrapper : startedPlugins) + { + if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId())) + { + continue; + } + + checkDepsAndStart(startedPlugins, scannedPlugins, pluginWrapper); + } + + scanAndInstantiate(scannedPlugins, true, false); + + groups.broadcastSring("STARTEXTERNAL;" + pluginId); + + return true; + } + + public void receive(Message message) + { + if (message.getObject() instanceof ConfigChanged) + { + return; + } + + String[] messageObject = ((String) message.getObject()).split(";"); + + if (messageObject.length < 2) + { + return; + } + + String command = messageObject[0]; + String pluginId = messageObject[1]; + + switch (command) + { + case "STARTEXTERNAL": + externalPluginManager.loadPlugins(); + externalPluginManager.startPlugin(pluginId); + + List startedPlugins = ImmutableList.copyOf(getStartedPlugins()); + List scannedPlugins = new ArrayList<>(); + + for (PluginWrapper pluginWrapper : startedPlugins) + { + if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId())) + { + continue; + } + + checkDepsAndStart(startedPlugins, scannedPlugins, pluginWrapper); + } + + scanAndInstantiate(scannedPlugins, true, false); + + break; + + case "STOPEXTERNAL": + uninstall(pluginId, true); + externalPluginManager.unloadPlugin(pluginId); + groups.send(message.getSrc(), "STOPPEDEXTERNAL;" + pluginId); + break; + + case "STOPPEDEXTERNAL": + groups.getMessageMap().get(pluginId).remove(message.getSrc()); + + if (groups.getMessageMap().get(pluginId).size() == 0) + { + groups.getMessageMap().remove(pluginId); + externalPluginManager.deletePlugin(pluginId); + } + + break; + } } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java index 3f6f44ec81..94e7123da6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java @@ -63,5 +63,9 @@ public class PluginClassLoader extends URLClassLoader // fall back to main class loader return parent.loadClass(name); } + catch (NoClassDefFoundError ex) + { + return null; + } } } 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 cce5c04a42..8e81576c90 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 @@ -39,6 +39,8 @@ import com.google.inject.CreationException; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; +import io.reactivex.rxjava3.schedulers.Schedulers; +import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -56,6 +58,7 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import javax.swing.SwingUtilities; @@ -68,6 +71,7 @@ import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigManager; import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.eventbus.EventBus; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.PluginChanged; import net.runelite.client.events.SessionClose; import net.runelite.client.events.SessionOpen; @@ -76,6 +80,8 @@ import net.runelite.client.task.ScheduledMethod; import net.runelite.client.task.Scheduler; import net.runelite.client.ui.RuneLiteSplashScreen; import net.runelite.client.util.GameEventManager; +import net.runelite.client.util.Groups; +import org.jgroups.Message; @Singleton @Slf4j @@ -96,6 +102,8 @@ public class PluginManager private final List fakePlugins = new ArrayList<>(); private final String runeliteGroupName = RuneLiteConfig.class .getAnnotation(ConfigGroup.class).value(); + private final Groups groups; + private final File settingsFileInput; @Inject ExternalPluginLoader externalPluginLoader; @@ -109,18 +117,23 @@ public class PluginManager final EventBus eventBus, final Scheduler scheduler, final ConfigManager configManager, - final Provider sceneTileManager) + final Provider sceneTileManager, + final Groups groups, + final @Named("config") File config) { this.eventBus = eventBus; this.scheduler = scheduler; this.configManager = configManager; this.sceneTileManager = sceneTileManager; + this.groups = groups; + this.settingsFileInput = config; - if (eventBus != null) - { - eventBus.subscribe(SessionOpen.class, this, this::onSessionOpen); - eventBus.subscribe(SessionClose.class, this, this::onSessionClose); - } + eventBus.subscribe(SessionOpen.class, this, this::onSessionOpen); + eventBus.subscribe(SessionClose.class, this, this::onSessionClose); + + groups.getMessageStringSubject() + .subscribeOn(Schedulers.from(SwingUtilities::invokeLater)) + .subscribe(this::receive); } private void onSessionOpen(SessionOpen event) @@ -321,6 +334,12 @@ public class PluginManager for (ClassInfo classInfo : classes) { Class clazz = classInfo.load(); + + if (clazz == null) + { + continue; + } + PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class); if (pluginDescriptor == null) @@ -462,6 +481,8 @@ public class PluginManager throw new PluginInstantiationException(ex); } + groups.broadcastSring("STARTPLUGIN;" + plugin.getClass().getSimpleName() + ";" + settingsFileInput.getAbsolutePath()); + return true; } @@ -491,6 +512,8 @@ public class PluginManager throw new PluginInstantiationException(ex); } + groups.broadcastSring("STOPPLUGIN;" + plugin.getClass().getSimpleName() + ";" + settingsFileInput.getAbsolutePath()); + return true; } @@ -661,4 +684,77 @@ public class PluginManager incrementChildren(graph, dependencyCount, m, val + 1)); } } + + public void receive(Message message) + { + if (message.getObject() instanceof ConfigChanged) + { + return; + } + + String[] messageObject = ((String) message.getObject()).split(";"); + + if (messageObject.length < 3) + { + return; + } + + String command = messageObject[0]; + String pluginName = messageObject[1]; + String path = messageObject[2]; + Plugin plugin = null; + + if (!path.equals(settingsFileInput.getAbsolutePath())) + { + return; + } + + for (Plugin pl : getPlugins()) + { + if (pl.getClass().getSimpleName().equals(pluginName)) + { + plugin = pl; + + break; + } + } + + if (plugin == null) + { + return; + } + + Plugin finalPlugin = plugin; + + switch (command) + { + case "STARTPLUGIN": + + try + { + startPlugin(finalPlugin); + } + catch (PluginInstantiationException e) + { + log.warn("unable to start plugin", e); + throw new RuntimeException(e); + } + + break; + + case "STOPPLUGIN": + + try + { + stopPlugin(finalPlugin); + } + catch (PluginInstantiationException e) + { + log.warn("unable to stop plugin", e); + throw new RuntimeException(e); + } + + break; + } + } } 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 c4c687eb86..8a8f49dc6a 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 @@ -30,6 +30,7 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.Font; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionListener; @@ -78,6 +79,7 @@ import javax.swing.plaf.basic.BasicSpinnerUI; import javax.swing.text.JTextComponent; import lombok.extern.slf4j.Slf4j; import net.runelite.api.util.Text; +import net.runelite.client.RuneLite; import net.runelite.client.config.Button; import net.runelite.client.config.ConfigDescriptor; import net.runelite.client.config.ConfigItem; @@ -90,7 +92,9 @@ import net.runelite.client.config.ModifierlessKeybind; import net.runelite.client.config.Range; import net.runelite.client.config.Units; import net.runelite.client.eventbus.EventBus; +import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.PluginChanged; +import net.runelite.client.plugins.ExternalPluginManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.DynamicGridLayout; @@ -102,6 +106,7 @@ import net.runelite.client.ui.components.colorpicker.ColorPickerManager; import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker; import net.runelite.client.util.ColorUtil; import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.LinkBrowser; import net.runelite.client.util.MiscUtils; import net.runelite.client.util.SwingUtil; @@ -132,6 +137,9 @@ class ConfigPanel extends PluginPanel @Inject private PluginManager pluginManager; + @Inject + private ExternalPluginManager externalPluginManager; + @Inject private ColorPickerManager colorPickerManager; @@ -233,6 +241,7 @@ class ConfigPanel extends PluginPanel rebuild(false); eventBus.subscribe(PluginChanged.class, this, this::onPluginChanged); + eventBus.subscribe(ConfigChanged.class, this, this::onConfigChanged); } private void getSections(ConfigDescriptor cd) @@ -425,11 +434,44 @@ class ConfigPanel extends PluginPanel ConfigDescriptor cd = pluginConfig.getConfigDescriptor(); assert cd != null; + List buttons = new ArrayList<>(); + + Map> pluginsInfoMap = externalPluginManager.getPluginsInfoMap(); + + if (pluginConfig.getPlugin() != null && pluginsInfoMap.containsKey(pluginConfig.getPlugin().getClass().getSimpleName())) + { + + JPanel infoPanel = new JPanel(); + infoPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + infoPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + infoPanel.setLayout(new GridLayout(0, 1)); + + final Font smallFont = FontManager.getRunescapeSmallFont(); + + Map pluginInfo = pluginsInfoMap.get(pluginConfig.getPlugin().getClass().getSimpleName()); + + JLabel idLabel = new JLabel(htmlLabel("id", pluginInfo.get("id"))); + idLabel.setFont(smallFont); + infoPanel.add(idLabel); + + JLabel versionLabel = new JLabel(htmlLabel("version", pluginInfo.get("version"))); + versionLabel.setFont(smallFont); + infoPanel.add(versionLabel); + + JLabel providerLabel = new JLabel(htmlLabel("provider", pluginInfo.get("provider"))); + providerLabel.setFont(smallFont); + infoPanel.add(providerLabel); + + JButton button = new JButton("Support"); + button.addActionListener(e -> LinkBrowser.browse(pluginInfo.get("support"))); + buttons.add(button); + + mainPanel.add(infoPanel); + } + getSections(cd); getTitleSections(cd); - List buttons = new ArrayList<>(); - for (ConfigItemDescriptor cid : cd.getItems()) { if (cid == null) @@ -1168,4 +1210,25 @@ class ConfigPanel extends PluginPanel pluginToggle.setSelected(event.isLoaded())); } } + + public void onConfigChanged(ConfigChanged event) + { + if (event.getOrigin().equals(RuneLite.uuid)) + { + return; + } + + try + { + SwingUtilities.invokeAndWait(() -> rebuild(true)); + } + catch (InterruptedException | InvocationTargetException e) + { + } + } + + private static String htmlLabel(String key, String value) + { + return "" + key + ": " + value + ""; + } } \ No newline at end of file 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 272f195548..b744861106 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 @@ -34,13 +34,19 @@ import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JToggleButton; +import javax.swing.SwingWorker; import lombok.Getter; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.plugins.ExternalPluginManager; import net.runelite.client.plugins.PluginType; +import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; import net.runelite.client.util.ImageUtil; @@ -50,6 +56,8 @@ public class PluginListItem extends JPanel { private static final ImageIcon CONFIG_ICON; private static final ImageIcon CONFIG_ICON_HOVER; + private static final ImageIcon REFRESH_ICON; + private static final ImageIcon REFRESH_ICON_HOVER; private static final ImageIcon ON_STAR; private static final ImageIcon OFF_STAR; @@ -70,10 +78,13 @@ public class PluginListItem extends JPanel static { BufferedImage configIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "config_edit_icon.png"); + BufferedImage refreshIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "refresh.png"); BufferedImage onStar = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "star_on.png"); CONFIG_ICON = new ImageIcon(configIcon); + REFRESH_ICON = new ImageIcon(refreshIcon); ON_STAR = new ImageIcon(ImageUtil.recolorImage(onStar, ColorScheme.BRAND_BLUE)); CONFIG_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(configIcon, -100)); + REFRESH_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(refreshIcon, -100)); BufferedImage offStar = ImageUtil.luminanceScale( ImageUtil.grayscaleImage(onStar), @@ -82,7 +93,7 @@ public class PluginListItem extends JPanel OFF_STAR = new ImageIcon(offStar); } - PluginListItem(PluginListPanel pluginListPanel, PluginConfigurationDescriptor pluginConfig) + PluginListItem(PluginListPanel pluginListPanel, PluginConfigurationDescriptor pluginConfig, ExternalPluginManager externalPluginManager) { this.pluginListPanel = pluginListPanel; this.pluginConfig = pluginConfig; @@ -119,6 +130,55 @@ public class PluginListItem extends JPanel buttonPanel.setLayout(new GridLayout(1, 2)); add(buttonPanel, BorderLayout.LINE_END); + Map> pluginsInfoMap = externalPluginManager.getPluginsInfoMap(); + + if (RuneLiteProperties.getLauncherVersion() == null && pluginConfig.getPlugin() != null && pluginsInfoMap.containsKey(pluginConfig.getPlugin().getClass().getSimpleName())) + { + JButton hotSwapButton = new JButton(REFRESH_ICON); + hotSwapButton.setRolloverIcon(REFRESH_ICON_HOVER); + SwingUtil.removeButtonDecorations(hotSwapButton); + hotSwapButton.setPreferredSize(new Dimension(25, 0)); + hotSwapButton.setVisible(false); + buttonPanel.add(hotSwapButton); + + hotSwapButton.addActionListener(e -> + { + Map pluginInfo = pluginsInfoMap.get(pluginConfig.getPlugin().getClass().getSimpleName()); + String pluginId = pluginInfo.get("id"); + + hotSwapButton.setIcon(REFRESH_ICON); + externalPluginManager.uninstall(pluginId); + + SwingWorker worker = new SwingWorker<>() + { + @Override + protected Boolean doInBackground() + { + return externalPluginManager.uninstall(pluginId); + } + }; + worker.execute(); + + JOptionPane.showMessageDialog(ClientUI.getFrame(), + pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`", + "Hotswap " + pluginId, + JOptionPane.INFORMATION_MESSAGE); + + worker = new SwingWorker<>() + { + @Override + protected Boolean doInBackground() + { + return externalPluginManager.reloadStart(pluginId); + } + }; + worker.execute(); + }); + + hotSwapButton.setVisible(true); + hotSwapButton.setToolTipText("Hotswap plugin"); + } + if (pluginConfig.hasConfigurables()) { JButton configButton = new JButton(CONFIG_ICON); @@ -211,8 +271,6 @@ public class PluginListItem extends JPanel */ static void addLabelMouseOver(final JLabel label) { - final Color labelForeground = label.getForeground(); - label.addMouseListener(new MouseAdapter() { private Color lastForeground; 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 e3a6516c3d..f06f9efeb8 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 @@ -62,6 +62,7 @@ import javax.swing.event.DocumentListener; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.util.Text; +import net.runelite.client.RuneLite; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigDescriptor; import net.runelite.client.config.ConfigGroup; @@ -73,6 +74,7 @@ import net.runelite.client.events.ConfigChanged; import net.runelite.client.events.ExternalPluginChanged; import net.runelite.client.events.ExternalPluginsLoaded; import net.runelite.client.events.PluginChanged; +import net.runelite.client.plugins.ExternalPluginManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.plugins.PluginInstantiationException; @@ -110,6 +112,7 @@ public class PluginListPanel extends PluginPanel private final PluginManager pluginManager; private final Provider configPanelProvider; private final OpenOSRSConfig openOSRSConfig; + private final ExternalPluginManager externalPluginManager; @Getter private final MultiplexingPluginPanel muxer; @@ -140,6 +143,7 @@ public class PluginListPanel extends PluginPanel PluginManager pluginManager, Provider configPanelProvider, OpenOSRSConfig openOSRSConfig, + ExternalPluginManager externalPluginManager, EventBus eventBus) { super(false); @@ -148,8 +152,14 @@ public class PluginListPanel extends PluginPanel this.pluginManager = pluginManager; this.configPanelProvider = configPanelProvider; this.openOSRSConfig = openOSRSConfig; + this.externalPluginManager = externalPluginManager; eventBus.subscribe(ConfigChanged.class, this, ev -> { + if (ev.getGroup().equals("runelite") && ev.getKey().equals("pinnedPlugins") && !ev.getOrigin().equals(RuneLite.uuid)) + { + SwingUtilities.invokeLater(this::rebuildPluginList); + } + if (!ev.getGroup().equals("openosrs")) { return; @@ -157,7 +167,7 @@ public class PluginListPanel extends PluginPanel if (ev.getKey().equals("enableCategories") || ev.getKey().equals("pluginSortMode")) { - rebuildPluginList(); + SwingUtilities.invokeLater(this::rebuildPluginList); } if (ev.getKey().equals("pluginSortMode")) @@ -264,7 +274,7 @@ public class PluginListPanel extends PluginPanel }) ).map(desc -> { - PluginListItem listItem = new PluginListItem(this, desc); + PluginListItem listItem = new PluginListItem(this, desc, externalPluginManager); listItem.setPinned(pinnedPlugins.contains(desc.getName())); listItem.setColor(getColorByCategory(listItem.getPluginType())); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java b/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java index cd16bc6f2b..923d955d51 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java @@ -67,6 +67,11 @@ public class RuneLiteSplashScreen extends JFrame this.setVisible(true); } + public static boolean showing() + { + return INSTANCE != null; + } + public static void setError(String title, String content) { if (INSTANCE != null) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/InfoPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/InfoPanel.java index 177ab845da..fc357d2f3f 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/components/InfoPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/InfoPanel.java @@ -34,9 +34,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; @@ -54,9 +51,6 @@ import net.runelite.client.util.LinkBrowser; @Slf4j public class InfoPanel extends JPanel { - private static final String RUNELITE_VERSION = "runelite.version"; - private static final String RUNELITE_PLUS_VERSION = "open.osrs.version"; - private static final String RUNELITE_PLUS_DATE = "open.osrs.builddate"; private static final Color DARK_GREY = new Color(10, 10, 10, 255); private static final BufferedImage TRANSPARENT_LOGO = ImageUtil.getResourceStreamFromClass(InfoPanel.class, "/openosrs.png"); @@ -67,16 +61,6 @@ public class InfoPanel extends JPanel public InfoPanel() { - Properties properties = new Properties(); - try (InputStream in = getClass().getResourceAsStream("/open.osrs.properties")) - { - properties.load(in); - } - catch (IOException ex) - { - log.warn("unable to load propertries", ex); - } - this.setLayout(new GridBagLayout()); this.setPreferredSize(PANEL_SIZE); this.setBackground(new Color(38, 38, 38)); @@ -104,11 +88,11 @@ public class InfoPanel extends JPanel c.weighty = 0; // OpenOSRS version - this.add(createPanelTextButton("OpenOSRS Version: " + properties.getProperty(RUNELITE_PLUS_VERSION)), c); + this.add(createPanelTextButton("OpenOSRS Version: " + RuneLiteProperties.getPlusVersion()), c); c.gridy++; // Build date - this.add(createPanelTextButton("Build date: " + properties.getProperty(RUNELITE_PLUS_DATE)), c); + this.add(createPanelTextButton("Build date: " + RuneLiteProperties.getPlusDate()), c); c.gridy++; final JLabel logsFolder = createPanelButton("Open logs folder", null, () -> LinkBrowser.openLocalFile(LOGS_DIR)); diff --git a/runelite-client/src/main/java/net/runelite/client/util/AppLock.java b/runelite-client/src/main/java/net/runelite/client/util/AppLock.java deleted file mode 100644 index aca0832439..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/util/AppLock.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2018 by rumatoest at github.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package net.runelite.client.util; - -/** - * The Class AppLock. - * - * @author Vladislav Zablotsky - */ -public class AppLock -{ - - private static CrossLock lockInstance; - - /** - * Set lock for application instance. - * Method must be run only one time at application start. - * - * @param lockId Unique lock identifiers - * @return true if succeeded - */ - public synchronized boolean lock(String lockId) - { - if (lockInstance == null) - { - lockInstance = new CrossLock("application_" + lockId); - } - return lockInstance.lock(); - } - - /** - * Trying to release application lock. - * Thus another application instances will be able to use lock with current ID. - */ - public synchronized void release() - { - if (lockInstance != null) - { - lockInstance.clear(); - } - lockInstance = null; - } -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java b/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java deleted file mode 100644 index 5db7a63432..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2018 by rumatoest at github.com - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package net.runelite.client.util; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.channels.OverlappingFileLockException; -import java.util.HashMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import static net.runelite.client.RuneLite.RUNELITE_DIR; - -/** - * Universal cross application instances locker. - * Allow you to create simple lock like object which can be used for - * different application instances. Basic idea is simple - just a simple file lock. - *
- * All you need is to define unique key for each lock type. - * - * @author Vladislav Zablotsky - */ -public class CrossLock -{ - - private static final HashMap locks = new HashMap<>(); - - private final String id; - - private final File fileToLock; - - private FileOutputStream fileStream; - - private FileChannel fileStreamChannel; - - private FileLock lockOnFile; - - /** - * Will create or retrieve lock instance. - * Each lock id is unique among all you instances, - * thus only one instance can acquire lock for this id. - * - * @param lockId Unique lock identifier - * @return Not null - */ - public static CrossLock get(String lockId) - { - if (locks.containsKey(lockId)) - { - return locks.get(lockId); - } - else - { - synchronized (CrossLock.class) - { - if (locks.containsKey(lockId)) - { - return locks.get(lockId); - } - else - { - CrossLock cl = new CrossLock(lockId); - locks.put(lockId, cl); - return cl; - } - } - } - } - - /** - * Will remove lock object for specific id and release lock if any. - * - * @param lockId Unique lock identifier - */ - public static void remove(String lockId) - { - if (locks.containsKey(lockId)) - { - CrossLock lock = null; - synchronized (CrossLock.class) - { - if (locks.containsKey(lockId)) - { - lock = locks.remove(lockId); - } - } - if (lock != null) - { - lock.release(); - } - } - } - - CrossLock(String lockId) - { - this.id = lockId; - fileToLock = new File(RUNELITE_DIR, lockId + ".app_lock"); - } - - /** - * Return lock instance identifier. - */ - public String id() - { - return this.id; - } - - /** - * Activate lock. - * Note! This is only cross application (cross instances) lock. It will not work - * as lock inside single application instance. - * - * @return true if lock was acquire or false - */ - public synchronized boolean lock() - { - if (lockOnFile != null && lockOnFile.isValid()) - { - return true; - } - else - { - release(); - } - - String lockContent = "#Java AppLock Object\n#Locked by key: " + id() + "\r\n"; - try - { - if (fileToLock.exists()) - { - fileToLock.createNewFile(); - } - fileStream = new FileOutputStream(fileToLock); - fileStreamChannel = fileStream.getChannel(); - lockOnFile = fileStreamChannel.tryLock(); - if (lockOnFile != null) - { - fileStream.write(lockContent.getBytes()); - } - } - catch (Exception ex) - { - if (!(ex instanceof OverlappingFileLockException)) - { - Logger.getLogger(AppLock.class.getName()).log(Level.WARNING, - "Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex); - } - return false; - } - - return lockOnFile != null; - } - - /** - * Release lock associated with this object. - */ - public synchronized void release() - { - try - { - if (lockOnFile != null && lockOnFile.isValid()) - { - lockOnFile.release(); - } - lockOnFile = null; - - if (fileStream != null) - { - fileStream.close(); - fileStream = null; - } - - if (fileStreamChannel != null && fileStreamChannel.isOpen()) - { - fileStreamChannel.close(); - } - fileStreamChannel = null; - } - catch (IOException ex) - { - Logger.getLogger(AppLock.class.getName()).log(Level.WARNING, - "Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex); - } - } - - /** - * Release lock and remove lock file. - */ - public synchronized void clear() - { - release(); - if (fileToLock.exists()) - { - fileToLock.delete(); - } - } - - @Override - protected void finalize() throws Throwable - { - this.clear(); - super.finalize(); - } - -} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/util/Groups.java b/runelite-client/src/main/java/net/runelite/client/util/Groups.java new file mode 100644 index 0000000000..2984bcae46 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/Groups.java @@ -0,0 +1,143 @@ +package net.runelite.client.util; + +import io.reactivex.rxjava3.subjects.PublishSubject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.config.OpenOSRSConfig; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.ui.RuneLiteSplashScreen; +import org.jgroups.Address; +import org.jgroups.JChannel; +import org.jgroups.Message; +import org.jgroups.ReceiverAdapter; +import org.jgroups.View; +import org.jgroups.util.Util; + +@Slf4j +@Singleton +public class Groups extends ReceiverAdapter +{ + private final OpenOSRSConfig openOSRSConfig; + private final JChannel channel; + + @Getter(AccessLevel.PUBLIC) + private int instanceCount; + @Getter(AccessLevel.PUBLIC) + private Map> messageMap = new HashMap<>(); + @Getter(AccessLevel.PUBLIC) + private final PublishSubject messageStringSubject = PublishSubject.create(); + @Getter(AccessLevel.PUBLIC) + private final PublishSubject messageObjectSubject = PublishSubject.create(); + + @Inject + public Groups(OpenOSRSConfig openOSRSConfig) throws Exception + { + this.openOSRSConfig = openOSRSConfig; + + this.channel = new JChannel(RuneLite.class.getResourceAsStream("/udp.xml")) + .setName(RuneLite.uuid) + .setReceiver(this) + .setDiscardOwnMessages(true) + .connect("openosrs"); + } + + public void broadcastSring(String command) + { + send(null, command); + } + + public void sendConfig(Address destination, ConfigChanged configChanged) + { + if (!openOSRSConfig.localSync() || RuneLiteSplashScreen.showing() || instanceCount < 2) + { + return; + } + + try + { + byte[] buffer = Util.objectToByteBuffer(configChanged); + Message message = new Message() + .setDest(destination) + .setBuffer(buffer); + + channel.send(message); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + public void sendString(String command) + { + String[] messageObject = command.split(";"); + String pluginId = messageObject[1]; + + messageMap.put(pluginId, new ArrayList<>()); + + for (Address member : channel.getView().getMembers()) + { + if (member.toString().equals(RuneLite.uuid)) + { + continue; + } + + messageMap.get(pluginId).add(member); + send(member, command); + } + } + + public void send(Address destination, String command) + { + if (!openOSRSConfig.localSync() || RuneLiteSplashScreen.showing() || instanceCount < 2) + { + return; + } + + try + { + channel.send(new Message(destination, command)); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + @Override + public void viewAccepted(View view) + { + instanceCount = view.getMembers().size(); + } + + @Override + public void receive(Message message) + { + if (RuneLiteSplashScreen.showing()) + { + return; + } + + if (message.getObject() instanceof String) + { + messageStringSubject.onNext(message); + } + else + { + messageObjectSubject.onNext(message); + } + } + + public void close() + { + channel.close(); + } +} diff --git a/runelite-client/src/main/resources/logback.xml b/runelite-client/src/main/resources/logback.xml index 2096038dec..b8ca95b9fb 100644 --- a/runelite-client/src/main/resources/logback.xml +++ b/runelite-client/src/main/resources/logback.xml @@ -69,4 +69,5 @@ + \ No newline at end of file diff --git a/runelite-client/src/main/resources/open.osrs.properties b/runelite-client/src/main/resources/net/runelite/client/open.osrs.properties similarity index 100% rename from runelite-client/src/main/resources/open.osrs.properties rename to runelite-client/src/main/resources/net/runelite/client/open.osrs.properties diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/config/refresh.png b/runelite-client/src/main/resources/net/runelite/client/plugins/config/refresh.png new file mode 100644 index 0000000000..cd26305f7a Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/config/refresh.png differ diff --git a/runelite-client/src/main/resources/udp.xml b/runelite-client/src/main/resources/udp.xml new file mode 100644 index 0000000000..fe42f0bd1f --- /dev/null +++ b/runelite-client/src/main/resources/udp.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java index c3ce340492..bd33ce2953 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java @@ -116,7 +116,7 @@ public class PluginManagerTest @Test public void testLoadPlugins() throws Exception { - PluginManager pluginManager = new PluginManager(null, null, null, null); + PluginManager pluginManager = new PluginManager(null, null, null, null, null, null); pluginManager.setOutdated(true); pluginManager.loadCorePlugins(); Collection plugins = pluginManager.getPlugins(); @@ -127,7 +127,7 @@ public class PluginManagerTest .count(); assertEquals(expected, plugins.size()); - pluginManager = new PluginManager(null, null, null, null); + pluginManager = new PluginManager(null, null, null, null, null, null); pluginManager.loadCorePlugins(); plugins = pluginManager.getPlugins(); @@ -145,7 +145,7 @@ public class PluginManagerTest modules.add(new GraphvizModule()); modules.add(new RuneLiteModule(() -> null, RuneLite.DEFAULT_CONFIG_FILE)); - PluginManager pluginManager = new PluginManager(null, null, null, null); + PluginManager pluginManager = new PluginManager(null, null, null, null, null, null); pluginManager.loadCorePlugins(); modules.addAll(pluginManager.getPlugins()); @@ -194,10 +194,10 @@ public class PluginManagerTest } @Test - public void testEventbusAnnotations() throws PluginInstantiationException + public void testEventbusAnnotations() throws Exception { EventBus eventbus = new EventBus(); - PluginManager pluginManager = new PluginManager(eventbus, null, null, null) + PluginManager pluginManager = new PluginManager(eventbus, null, null, null, null, null) { @Override public boolean isPluginEnabled(Plugin plugin)