runelite-client: add support for dynamically loaded plugins

This commit is contained in:
Adam
2017-11-18 14:41:05 -05:00
parent 027b495727
commit d047af14d0
11 changed files with 545 additions and 105 deletions

View File

@@ -43,6 +43,7 @@ import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import javax.imageio.ImageIO;
import javax.inject.Singleton;
@@ -60,6 +61,7 @@ import net.runelite.client.config.ConfigManager;
import net.runelite.client.events.SessionClose;
import net.runelite.client.events.SessionOpen;
import net.runelite.client.menus.MenuManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.ui.ClientUI;
import net.runelite.http.api.account.AccountClient;
@@ -75,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 SESSION_FILE = new File(RUNELITE_DIR, "session");
public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins");
public static Image ICON;
@@ -165,16 +168,20 @@ public class RuneLite
// Load the plugins, but does not start them yet.
// This will initialize configuration
pluginManager.loadPlugins();
List<Plugin> plugins = pluginManager.loadCorePlugins();
// Plugins have provided their config, so set default config
// to main settings
configManager.loadDefault();
// Start plugins
pluginManager.start();
pluginManager.startCorePlugins(plugins);
// Load the session, including saved configuration
loadSession();
// Begin watching for new plugins
pluginManager.watch();
}
public void setTitle(String extra)

View File

@@ -42,4 +42,6 @@ public @interface ConfigItem
String description();
boolean hidden() default false;
String confirmationWarining() default "";
}

View File

@@ -92,7 +92,7 @@ public class ConfigManager
{
List<Injector> injectors = new ArrayList<>();
injectors.add(RuneLite.getInjector());
pluginManager.getAllPlugins().forEach(pl -> injectors.add(pl.getInjector()));
pluginManager.getPlugins().forEach(pl -> injectors.add(pl.getInjector()));
List<Config> list = new ArrayList<>();
for (Injector injector : injectors)
@@ -235,7 +235,7 @@ public class ConfigManager
throw new RuntimeException("Non-public configuration classes can't have default methods invoked");
}
T t = (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[]
T t = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]
{
clazz
}, handler);
@@ -350,6 +350,10 @@ public class ConfigManager
return new ConfigDescriptor(group, items);
}
/**
* Initialize the configuration from the default settings
* @param proxy
*/
private void setDefaultConfiguration(Object proxy)
{
Class<?> clazz = proxy.getClass().getInterfaces()[0];

View File

@@ -40,4 +40,17 @@ public interface RuneliteConfig extends Config
{
return false;
}
@ConfigItem(
keyName = "enablePlugins",
name = "Enable loading of external plugins",
description = "Enable loading of external plugins",
confirmationWarining = "WARNING: Using untrusted third party plugins is a SECURITY RISK\n"
+ " and can result in loss of YOUR ACCOUNT, and compromise the security\n"
+ "of your computer. Are you sure you want to do this?"
)
default boolean enablePlugins()
{
return false;
}
}

View File

@@ -24,31 +24,29 @@
*/
package net.runelite.client.plugins;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Module;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Executor;
import javax.swing.SwingUtilities;
import net.runelite.client.ui.overlay.Overlay;
public abstract class Plugin extends AbstractIdleService implements Module
public abstract class Plugin implements Module
{
protected Injector injector;
File file;
PluginClassLoader loader;
@Override
public void configure(Binder binder)
{
}
@Override
protected void startUp() throws Exception
{
}
@Override
protected void shutDown() throws Exception
{
}
@@ -68,16 +66,4 @@ public abstract class Plugin extends AbstractIdleService implements Module
Overlay overlay = getOverlay();
return overlay != null ? Collections.singletonList(overlay) : Collections.EMPTY_LIST;
}
/**
* Override AbstractIdleService's default executor to instead execute in
* the AWT event dispatch thread.
*
* @return
*/
@Override
protected Executor executor()
{
return r -> SwingUtilities.invokeLater(r);
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
* 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);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
* 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;
public class PluginInstantiationException extends Exception
{
public PluginInstantiationException(Throwable cause)
{
super(cause);
}
}

View File

@@ -28,20 +28,20 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.ServiceManager;
import com.google.inject.Binder;
import com.google.inject.CreationException;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Singleton;
import javax.swing.SwingUtilities;
import net.runelite.client.RuneLite;
import net.runelite.client.task.Schedule;
import net.runelite.client.task.ScheduledMethod;
@@ -54,29 +54,59 @@ public class PluginManager
{
private static final Logger logger = LoggerFactory.getLogger(PluginManager.class);
/**
* Base package where the core plugins are
*/
private static final String PLUGIN_PACKAGE = "net.runelite.client.plugins";
@Inject
private EventBus eventBus;
EventBus eventBus;
@Inject
private Scheduler scheduler;
Scheduler scheduler;
private ServiceManager manager;
private final List<Plugin> plugins = new ArrayList<>();
@Inject
PluginWatcher pluginWatcher;
public void loadPlugins() throws IOException
private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
public List<Plugin> loadCorePlugins() throws IOException
{
boolean developerPlugins = false;
if (RuneLite.getOptions().has("developer-mode"))
return scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE);
}
public void startCorePlugins(List<Plugin> scannedPlugins)
{
for (Plugin plugin : scannedPlugins)
{
logger.info("Loading developer plugins");
developerPlugins = true;
try
{
startPlugin(plugin);
}
catch (PluginInstantiationException ex)
{
logger.warn("Unable to start plugin {}", plugin.getClass().getSimpleName(), ex);
continue;
}
plugins.add(plugin);
}
}
ClassPath classPath = ClassPath.from(getClass().getClassLoader());
public void watch()
{
pluginWatcher.start();
}
ImmutableSet<ClassInfo> classes = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE);
List<Plugin> scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException
{
boolean developerPlugins = RuneLite.getOptions().has("developer-mode");
List<Plugin> scannedPlugins = new ArrayList<>();
ClassPath classPath = ClassPath.from(classLoader);
ImmutableSet<ClassInfo> classes = packageName == null ? classPath.getAllClasses()
: classPath.getTopLevelClassesRecursive(packageName);
for (ClassInfo classInfo : classes)
{
Class<?> clazz = classInfo.load();
@@ -107,16 +137,88 @@ public class PluginManager
Plugin plugin;
try
{
plugin = (Plugin) clazz.newInstance();
plugin = instantiate(pluginDescriptor, (Class<Plugin>) clazz);
}
catch (InstantiationException | IllegalAccessException ex)
catch (PluginInstantiationException ex)
{
logger.warn("error initializing plugin", ex);
logger.warn("error instantiating plugin!", ex);
continue;
}
plugins.add(plugin);
scannedPlugins.add(plugin);
}
return scannedPlugins;
}
void startPlugin(Plugin plugin) throws PluginInstantiationException
{
try
{
// plugins always start in the event thread
SwingUtilities.invokeAndWait(() ->
{
try
{
plugin.startUp();
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
});
logger.debug("Plugin {} is now running", plugin.getClass().getSimpleName());
eventBus.register(plugin);
schedule(plugin);
}
catch (InterruptedException | InvocationTargetException ex)
{
throw new PluginInstantiationException(ex);
}
}
void stopPlugin(Plugin plugin) throws PluginInstantiationException
{
try
{
unschedule(plugin);
eventBus.unregister(plugin);
// plugins always stop in the event thread
SwingUtilities.invokeAndWait(() ->
{
try
{
plugin.shutDown();
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
});
}
catch (InterruptedException | InvocationTargetException ex)
{
throw new PluginInstantiationException(ex);
}
}
Plugin instantiate(PluginDescriptor pluginDescriptor, Class<Plugin> clazz) throws PluginInstantiationException
{
Plugin plugin;
try
{
plugin = (Plugin) clazz.newInstance();
}
catch (InstantiationException | IllegalAccessException ex)
{
throw new PluginInstantiationException(ex);
}
try
{
Module pluginModule = (Binder binder) ->
{
binder.bind((Class<Plugin>) clazz).toInstance(plugin);
@@ -125,78 +227,29 @@ public class PluginManager
Injector pluginInjector = RuneLite.getInjector().createChildInjector(pluginModule);
pluginInjector.injectMembers(plugin);
plugin.injector = pluginInjector;
logger.debug("Loaded plugin {}", pluginDescriptor.name());
}
}
public void start()
{
// Add plugin listeners
for (Plugin plugin : plugins)
catch (CreationException ex)
{
Service.Listener listener = new Service.Listener()
{
@Override
public void running()
{
logger.debug("Plugin {} is now running", plugin);
eventBus.register(plugin);
schedule(plugin);
}
@Override
public void stopping(Service.State from)
{
logger.debug("Plugin {} is stopping", plugin);
eventBus.unregister(plugin);
unschedule(plugin);
}
@Override
public void failed(Service.State from, Throwable failure)
{
logger.warn("Plugin {} has failed", plugin, failure);
if (from == Service.State.RUNNING)
{
eventBus.unregister(plugin);
unschedule(plugin);
}
}
};
plugin.addListener(listener, MoreExecutors.directExecutor());
throw new PluginInstantiationException(ex);
}
manager = new ServiceManager(plugins);
logger.debug("Starting plugins...");
manager.startAsync();
logger.debug("Loaded plugin {}", pluginDescriptor.name());
return plugin;
}
/**
* Get all plugins regardless of state
*
* @return
*/
public Collection<Plugin> getAllPlugins()
void add(Plugin plugin)
{
return plugins;
plugins.add(plugin);
}
void remove(Plugin plugin)
{
plugins.remove(plugin);
}
/**
* Get running plugins
*
* @return
*/
public Collection<Plugin> getPlugins()
{
return manager.servicesByState().get(Service.State.RUNNING)
.stream()
.map(s -> (Plugin) s)
.collect(Collectors.toList());
return plugins;
}
private void schedule(Plugin plugin)

View File

@@ -0,0 +1,255 @@
/*
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
* 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.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 net.runelite.client.RuneLite;
import net.runelite.client.config.RuneliteConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class PluginWatcher extends Thread
{
private static final Logger logger = LoggerFactory.getLogger(PluginWatcher.class);
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
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());
logger.debug("Event {} file {}", kind, file);
if (kind == ENTRY_MODIFY)
{
Plugin existing = findPluginForFile(file);
if (existing != null)
{
logger.info("Reloading plugin {}", file);
unload(existing);
}
else
{
logger.info("Loading plugin {}", file);
}
load(file);
}
else if (kind == ENTRY_DELETE)
{
Plugin existing = findPluginForFile(file);
if (existing != null)
{
logger.info("Unloading plugin {}", file);
unload(existing);
}
}
}
key.reset();
}
catch (InterruptedException ex)
{
logger.warn("error polling for plugins", ex);
}
}
}
private void scan()
{
for (File file : BASE.listFiles())
{
if (!file.getName().endsWith(".jar"))
{
continue;
}
logger.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)
{
logger.warn("Error loading plugin", ex);
return;
}
List<Plugin> loadedPlugins;
try
{
loadedPlugins = pluginManager.scanAndInstantiate(loader, null);
}
catch (IOException ex)
{
close(loader);
logger.warn("Error loading plugin", ex);
return;
}
if (loadedPlugins.isEmpty())
{
close(loader);
logger.warn("No plugin found in plugin {}", pluginFile);
return;
}
if (loadedPlugins.size() != 1)
{
close(loader);
logger.warn("You can not have more than one plugin per jar");
return;
}
Plugin plugin = loadedPlugins.get(0);
plugin.file = pluginFile;
plugin.loader = loader;
try
{
pluginManager.startPlugin(plugin);
}
catch (PluginInstantiationException ex)
{
close(loader);
logger.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)
{
logger.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)
{
logger.warn(null, ex1);
}
}
}

View File

@@ -49,6 +49,10 @@ import javax.swing.JComponent;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import static javax.swing.JOptionPane.WARNING_MESSAGE;
import static javax.swing.JOptionPane.YES_NO_OPTION;
import static javax.swing.JOptionPane.YES_OPTION;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
@@ -58,6 +62,7 @@ import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import net.runelite.client.config.ConfigDescriptor;
import net.runelite.client.config.ConfigItem;
import net.runelite.client.config.ConfigItemDescriptor;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.ui.PluginPanel;
@@ -136,9 +141,23 @@ public class ConfigPanel extends PluginPanel
private void changeConfiguration(JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid)
{
ConfigItem configItem = cid.getItem();
if (component instanceof JCheckBox)
{
JCheckBox checkbox = (JCheckBox) component;
if (checkbox.isSelected() && !configItem.confirmationWarining().isEmpty())
{
int value = JOptionPane.showOptionDialog(component, configItem.confirmationWarining(),
"Are you sure?", YES_NO_OPTION, WARNING_MESSAGE,
null, new String[] { "Yes", "No" }, "No");
if (value != YES_OPTION)
{
checkbox.setSelected(false);
return;
}
}
configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + checkbox.isSelected());
}

View File

@@ -74,7 +74,7 @@ public class PluginManagerTest
public void testLoadPlugins() throws Exception
{
PluginManager pluginManager = new PluginManager();
pluginManager.loadPlugins();
pluginManager.loadCorePlugins();
}
@Test
@@ -85,8 +85,8 @@ public class PluginManagerTest
modules.add(new RuneliteModule());
PluginManager pluginManager = new PluginManager();
pluginManager.loadPlugins();
for (Plugin p : pluginManager.getAllPlugins())
pluginManager.loadCorePlugins();
for (Plugin p : pluginManager.getPlugins())
{
modules.add(p);
}