diff --git a/pom.xml b/pom.xml
index 5d9386effe..2122c9f537 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,7 @@
true
true
-
+ true
179
@@ -120,6 +120,7 @@
runelite-mixins
runelite-script-assembler-plugin
runescape-api
+ runelite-plugin-archetype
http-api
http-service
protocol-api
@@ -151,6 +152,13 @@
pom
import
+
+ org.apache.maven.archetype
+ archetype-packaging
+ 3.0.1
+ pom
+ import
+
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 ba3169020d..74594a8ed4 100644
--- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java
+++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java
@@ -77,6 +77,7 @@ public class RuneLite
{
public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite");
public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles");
+ public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins");
public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots");
@Getter
@@ -243,6 +244,9 @@ public class RuneLite
// Load the session, including saved configuration
sessionManager.loadSession();
+ // Begin watching for new plugins
+ pluginManager.watch();
+
// Tell the plugin manager if client is outdated or not
pluginManager.setOutdated(isOutdated);
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 546f7e77bc..da48d2d116 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
@@ -63,6 +63,17 @@ public interface RuneLiteConfig extends Config
return false;
}
+ @ConfigItem(
+ keyName = "enablePlugins",
+ name = "Enable loading of external plugins",
+ description = "Enable loading of external plugins",
+ position = 10
+ )
+ default boolean enablePlugins()
+ {
+ return false;
+ }
+
@ConfigItem(
keyName = "containInScreen",
name = "Contain in screen",
@@ -272,4 +283,4 @@ public interface RuneLiteConfig extends Config
{
return 35;
}
-}
\ No newline at end of file
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java
index be5efa9081..192f587665 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java
@@ -28,10 +28,15 @@ import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Module;
+import java.io.File;
+
public abstract class Plugin implements Module
{
protected Injector injector;
+ public File file;
+ public PluginClassLoader loader;
+
@Override
public void configure(Binder binder)
{
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
new file mode 100644
index 0000000000..3f6f44ec81
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016-2017, Adam
+ * 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 java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+/**
+ * A classloader for external plugins
+ *
+ * @author Adam
+ */
+public class PluginClassLoader extends URLClassLoader
+{
+ private final ClassLoader parent;
+
+ public PluginClassLoader(File plugin, ClassLoader parent) throws MalformedURLException
+ {
+ super(
+ new URL[]
+ {
+ plugin.toURI().toURL()
+ },
+ null // null or else class path scanning includes everything from the main class loader
+ );
+
+ this.parent = parent;
+ }
+
+ @Override
+ public Class> loadClass(String name) throws ClassNotFoundException
+ {
+ try
+ {
+ return super.loadClass(name);
+ }
+ catch (ClassNotFoundException ex)
+ {
+ // fall back to main class loader
+ return parent.loadClass(name);
+ }
+ }
+}
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 73d6bf32e4..62e8a22aa8 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
@@ -92,6 +92,9 @@ public class PluginManager
private final String runeliteGroupName = RuneLiteConfig.class
.getAnnotation(ConfigGroup.class).value();
+ @Inject
+ PluginWatcher pluginWatcher;
+
@Setter
boolean isOutdated;
@@ -113,6 +116,11 @@ public class PluginManager
this.sceneTileManager = sceneTileManager;
}
+ public void watch()
+ {
+ pluginWatcher.start();
+ }
+
@Subscribe
public void onSessionOpen(SessionOpen event)
{
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java
new file mode 100644
index 0000000000..031d408c03
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2016-2017, Adam
+ * 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.inject.Injector;
+import com.google.inject.Key;
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URLClassLoader;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.List;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.client.RuneLite;
+import net.runelite.client.config.Config;
+import net.runelite.client.config.ConfigManager;
+import net.runelite.client.config.RuneLiteConfig;
+
+@Singleton
+@Slf4j
+public class PluginWatcher extends Thread
+{
+ private static final File BASE = RuneLite.PLUGIN_DIR;
+
+ private final RuneLiteConfig runeliteConfig;
+ private final PluginManager pluginManager;
+ private final WatchService watchService;
+ private final WatchKey watchKey;
+
+ @Inject
+ private ConfigManager configManager;
+
+ @Inject
+ public PluginWatcher(RuneLiteConfig runeliteConfig, PluginManager pluginManager) throws IOException
+ {
+ this.runeliteConfig = runeliteConfig;
+ this.pluginManager = pluginManager;
+
+ setName("Plugin Watcher");
+ setDaemon(true);
+
+ watchService = FileSystems.getDefault().newWatchService();
+ BASE.mkdirs();
+ Path dir = BASE.toPath();
+ watchKey = dir.register(watchService, ENTRY_MODIFY, ENTRY_DELETE);
+ }
+
+ public void cancel()
+ {
+ watchKey.cancel();
+ }
+
+ @Override
+ public void run()
+ {
+ if (runeliteConfig.enablePlugins())
+ {
+ scan();
+ }
+
+ for (;;)
+ {
+ try
+ {
+ WatchKey key = watchService.take();
+ Thread.sleep(50);
+
+ if (!runeliteConfig.enablePlugins())
+ {
+ key.reset();
+ continue;
+ }
+
+ for (WatchEvent> event : key.pollEvents())
+ {
+ Kind> kind = event.kind();
+ Path path = (Path) event.context();
+ File file = new File(BASE, path.toFile().getName());
+
+ log.debug("Event {} file {}", kind, file);
+
+ if (kind == ENTRY_MODIFY)
+ {
+ Plugin existing = findPluginForFile(file);
+ if (existing != null)
+ {
+ log.info("Reloading plugin {}", file);
+ unload(existing);
+ }
+ else
+ {
+ log.info("Loading plugin {}", file);
+ }
+
+ load(file);
+ }
+ else if (kind == ENTRY_DELETE)
+ {
+ Plugin existing = findPluginForFile(file);
+ if (existing != null)
+ {
+ log.info("Unloading plugin {}", file);
+
+ unload(existing);
+ }
+ }
+ }
+ key.reset();
+
+ }
+ catch (InterruptedException ex)
+ {
+ log.warn("error polling for plugins", ex);
+ }
+ }
+ }
+
+ private void scan()
+ {
+ for (File file : BASE.listFiles())
+ {
+ if (!file.getName().endsWith(".jar"))
+ {
+ continue;
+ }
+
+ log.info("Loading plugin from {}", file);
+ load(file);
+ }
+ }
+
+ private Plugin findPluginForFile(File file)
+ {
+ for (Plugin plugin : pluginManager.getPlugins())
+ {
+ if (plugin.file != null && plugin.file.equals(file))
+ {
+ return plugin;
+ }
+ }
+ return null;
+ }
+
+ private void load(File pluginFile)
+ {
+ PluginClassLoader loader;
+ try
+ {
+ loader = new PluginClassLoader(pluginFile, getClass().getClassLoader());
+ }
+ catch (MalformedURLException ex)
+ {
+ log.warn("Error loading plugin", ex);
+ return;
+ }
+
+ List loadedPlugins;
+ try
+ {
+ loadedPlugins = pluginManager.scanAndInstantiate(loader, null);
+ }
+ catch (IOException ex)
+ {
+ close(loader);
+ log.warn("Error loading plugin", ex);
+ return;
+ }
+
+ if (loadedPlugins.isEmpty())
+ {
+ close(loader);
+ log.warn("No plugin found in plugin {}", pluginFile);
+ return;
+ }
+
+ if (loadedPlugins.size() != 1)
+ {
+ close(loader);
+ log.warn("You can not have more than one plugin per jar");
+ return;
+ }
+
+ Plugin plugin = loadedPlugins.get(0);
+ plugin.file = pluginFile;
+ plugin.loader = loader;
+
+ // Initialize default configuration
+ Injector injector = plugin.getInjector();
+ for (Key> key : injector.getAllBindings().keySet())
+ {
+ Class> type = key.getTypeLiteral().getRawType();
+ if (Config.class.isAssignableFrom(type))
+ {
+ Config config = (Config) injector.getInstance(key);
+ configManager.setDefaultConfiguration(config, false);
+ }
+ }
+
+ try
+ {
+ pluginManager.startPlugin(plugin);
+ }
+ catch (PluginInstantiationException ex)
+ {
+ close(loader);
+ log.warn("unable to start plugin", ex);
+ return;
+ }
+
+ // Plugin is now running
+ pluginManager.add(plugin);
+ }
+
+ private void unload(Plugin plugin)
+ {
+ try
+ {
+ pluginManager.stopPlugin(plugin);
+ }
+ catch (PluginInstantiationException ex)
+ {
+ log.warn("unable to stop plugin", ex);
+ }
+
+ pluginManager.remove(plugin); // remove it regardless
+
+ close(plugin.loader);
+ }
+
+ private void close(URLClassLoader classLoader)
+ {
+ try
+ {
+ classLoader.close();
+ }
+ catch (IOException ex1)
+ {
+ log.warn(null, ex1);
+ }
+ }
+
+}
diff --git a/runelite-mixins/pom.xml b/runelite-mixins/pom.xml
index e8045675a8..b8b5413e1e 100644
--- a/runelite-mixins/pom.xml
+++ b/runelite-mixins/pom.xml
@@ -82,8 +82,8 @@
- 1.6
- 1.6
+ 1.7
+ 1.7
diff --git a/runelite-plugin-archetype/pom.xml b/runelite-plugin-archetype/pom.xml
new file mode 100644
index 0000000000..fcb26cedf6
--- /dev/null
+++ b/runelite-plugin-archetype/pom.xml
@@ -0,0 +1,86 @@
+
+
+
+ 4.0.0
+
+
+ net.runelite
+ runelite-parent
+ 1.5.21-SNAPSHOT
+
+
+ runelite-plugin-archetype
+ maven-archetype
+ RuneLite Plugin Archetype
+
+
+
+
+
+ src/main/resources
+ true
+
+ archetype-resources/pom.xml
+
+
+
+ src/main/resources
+ false
+
+ archetype-resources/pom.xml
+
+
+
+
+
+
+ org.apache.maven.archetype
+ archetype-packaging
+ 3.0.1
+
+
+
+
+
+
+ maven-archetype-plugin
+ 3.0.1
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.0.2
+
+
+ \
+
+
+
+
+
+
diff --git a/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000000..b67d8cde67
--- /dev/null
+++ b/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ src/main/java
+
+ **/*.java
+
+
+
+
diff --git a/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000000..11882b973e
--- /dev/null
+++ b/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+ \${groupId}
+ \${artifactId}
+ \${version}
+ jar
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+ runelite
+ RuneLite
+ http://repo.runelite.net
+
+
+
+
+
+ net.runelite
+ client
+ ${project.version}
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 1.8
+
+
+ install
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
diff --git a/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties
new file mode 100644
index 0000000000..ace9df8523
--- /dev/null
+++ b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties
@@ -0,0 +1,5 @@
+sourceEncoding=UTF-8
+groupId=org.example.runelite
+artifactId=exampleplugin
+version=1.0.0-SNAPSHOT
+package=org.example.runelite.exampleplugin
\ No newline at end of file
diff --git a/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt
new file mode 100644
index 0000000000..0b5987362f
--- /dev/null
+++ b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt
@@ -0,0 +1 @@
+verify
diff --git a/travis/settings.xml b/travis/settings.xml
index 6d2fec47d7..06f13271e0 100644
--- a/travis/settings.xml
+++ b/travis/settings.xml
@@ -259,6 +259,7 @@ under the License.
true
true
+ false