package com.bergerkiller.bukkit.common; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import com.bergerkiller.bukkit.common.config.BasicConfiguration; import com.bergerkiller.bukkit.common.config.ConfigurationNode; import com.bergerkiller.bukkit.common.config.FileConfiguration; import com.bergerkiller.bukkit.common.internal.CommonPlugin; import com.bergerkiller.bukkit.common.localization.ILocalizationDefault; import com.bergerkiller.bukkit.common.metrics.Metrics; import com.bergerkiller.bukkit.common.permissions.IPermissionDefault; import com.bergerkiller.bukkit.common.permissions.NoPermissionException; import com.bergerkiller.bukkit.common.protocol.PacketListener; import com.bergerkiller.bukkit.common.protocol.PacketMonitor; import com.bergerkiller.bukkit.common.protocol.PacketType; import com.bergerkiller.bukkit.common.reflection.classes.PluginDescriptionFileRef; import com.bergerkiller.bukkit.common.utils.CommonUtil; import com.bergerkiller.bukkit.common.utils.LogicUtil; import com.bergerkiller.bukkit.common.utils.MathUtil; import com.bergerkiller.bukkit.common.utils.PacketUtil; import com.bergerkiller.bukkit.common.utils.ParseUtil; import com.bergerkiller.bukkit.common.utils.StringUtil; /** * The extended javaPlugin base used to communicate with BKCommonLib<br><br> * * Handles dependencies, command registration, event listener registration, * permissions and permission defaults, logging, error handling and localization. */ public abstract class PluginBase extends JavaPlugin { private String disableMessage, enableMessage; private FileConfiguration permissionconfig, localizationconfig; private final BasicConfiguration pluginYaml = new BasicConfiguration(); private boolean enabled = false; private boolean wasDisableRequested = false; private Metrics metrics; /** * Gets the logger for a specific module in this Plugin * * @param modulePath for the module * @return a new Module Logger */ public ModuleLogger getModuleLogger(String... modulePath) { return new ModuleLogger(this, modulePath); } /** * Logs a message to the server console * * @param level of the message * @param message to log */ public void log(Level level, String message) { this.getLogger().log(level, message); } /** * Logs the action of a certain player * * @param by whome the action was performed (only logged if it is a player) * @param action the player performed */ public void logAction(CommandSender by, String action) { if (by instanceof Player) { log(Level.INFO, ((Player) by).getName() + " " + action); } } /** * Gets the version of this Plugin * * @return Plugin version */ public final String getVersion() { return this.getDescription().getVersion(); } /** * Gets the version of this Plugin parsed into a major-minor number. * This expects the version of the plugin to be formatted like <b>MAJOR.MINOR.REVISION.BUILD</b> * with separate parts not exceeding 100.<br> * <b>REVISION and BUILD will not be contained in this version number!</b><br><br> * * Examples:<br> * - v1.0 = 100<br> * - v8.6 = 860<br> * - v8.06 = 806<br> * - v1.0.0 = 100<br> * - v1.81.65 = 181 * * @return version parsed to an Integer */ public int getVersionNumber() { String ver = this.getVersion(); // Get first dot index int dotIndex = ver.indexOf('.'); if (dotIndex != -1) { // Get second dot index from first dot index dotIndex = ver.indexOf('.', dotIndex + 1); if (dotIndex != -1) { // Trim this trailing part ver = ver.substring(0, dotIndex); } } return (int) (100.0 * ParseUtil.parseDouble(ver, 1.0)); } /** * Gets the file of the path relative to this plugin's data folder * * @param path of the file * @return relative data File */ public File getDataFile(String... path) { if (path == null || path.length == 0) { return this.getDataFolder(); } return new File(this.getDataFolder(), StringUtil.join(File.separator, path)); } /** * Gets a Permission, creates one if it doesn't exist * * @param path of the Permission to obtain * @return Permission */ public static Permission getPermission(String path) { return CommonPlugin.getInstance().getPermissionHandler().getPermission(path); } /** * Gets a permission configuration node * * @param path of the node to get * @return Permission configuration node */ public final ConfigurationNode getPermissionNode(String path) { return this.permissionconfig.getNode(path); } /** * Gets a localization configuration node * * @param path of the node to get * @return Localization configuration node */ public final ConfigurationNode getLocalizationNode(String path) { return this.localizationconfig.getNode(path); } /** * Registers this main class for one or more commands * * @param commands to register this Plugin class for */ public final void register(String... commands) { this.register(this, commands); } /** * Registers a command executor for one or more commands * * @param executor to register * @param commands to register it for */ public final void register(CommandExecutor executor, String... commands) { for (String command : commands) { PluginCommand cmd = this.getCommand(command); if (cmd != null) { cmd.setExecutor(executor); } } } /** * Registers a listener instance * * @param listener to register */ public final void register(Listener listener) { if (listener == null) { throw new RuntimeException("Can not load a listener: The listener instance is null"); } if (listener != this) { Bukkit.getPluginManager().registerEvents(listener, this); } } /** * Registers a listener class * * @param listener class to register */ public final void register(Class<? extends Listener> listener) { if (listener == null) { throw new RuntimeException("Can not load a listener: The listener class is null"); } try { this.register(listener.newInstance()); } catch (Exception e) { e.printStackTrace(); } } /** * Registers a packet monitor for the packet types specified. * Monitors can only monitor packets, they can not alter them. * * @param packetMonitor to register * @param packetTypes to register the listener for */ public final void register(PacketMonitor packetMonitor, PacketType... packetTypes) { PacketUtil.addPacketMonitor(this, packetMonitor, packetTypes); } /** * Registers a packet listener for the packet types specified. * Listeners are able to modify packets. * * @param packetListener to register * @param packetTypes to register the listener for */ public final void register(PacketListener packetListener, PacketType... packetTypes) { PacketUtil.addPacketListener(this, packetListener, packetTypes); } /** * Unregisters a packet listener * * @param packetListener to unregister */ public final void unregister(PacketListener packetListener) { PacketUtil.removePacketListener(packetListener); } /** * Loads all the permissions from a Permissions container class<br> * If the class is not an enumeration, the static constants in the class are used instead * * @param permissionDefaults class */ public final void loadPermissions(Class<? extends IPermissionDefault> permissionDefaults) { for (IPermissionDefault def : CommonUtil.getClassConstants(permissionDefaults)) { this.loadPermission(def); } } /** * Loads a single permission using a permission default * * @param permissionDefault to use * @return Permission that was loaded */ public final Permission loadPermission(IPermissionDefault permissionDefault) { return this.loadPermission(permissionDefault.getName(), permissionDefault.getDefault(), permissionDefault.getDescription()); } /** * Loads a single permission using a permission path * * @param path of the Permission * @return Permission that was loaded */ public final Permission loadPermission(String path) { return this.loadPermission(getPermission(path)); } /** * Loads a single permission using a Permission * * @param permission to load * @return Permission that was loaded */ public final Permission loadPermission(Permission permission) { return this.loadPermission(permission.getName(), permission.getDefault(), permission.getDescription()); } /** * Loads a single permission using the path, default and description * * @param path of the Permission * @param def value of the Permission * @param description value of the Permission * @return Permission that was loaded */ public final Permission loadPermission(String path, PermissionDefault def, String description) { return this.loadPermission(getPermissionNode(path), def, description); } /** * Loads a single permission using the configuration node, default and description * * @param node to use for the permission path, default and description * @param def value to use if the node is unusable * @param description to use if the node is unusable * @return Permission that was loaded */ public final Permission loadPermission(ConfigurationNode node, PermissionDefault def, String description) { Permission permission = getPermission(node.getPath()); permission.setDefault(node.get("default", def)); permission.setDescription(node.get("description", description)); return permission; } /** * Loads all the localization defaults from a Localization container class<br> * If the class is not an enumeration, the static constants in the class are used instead * * @param localizationDefaults class */ public void loadLocales(Class<? extends ILocalizationDefault> localizationDefaults) { for (ILocalizationDefault def : CommonUtil.getClassConstants(localizationDefaults)) { this.loadLocale(def); } } /** * Loads a localization using a localization default * * @param localizationDefault to load from */ public void loadLocale(ILocalizationDefault localizationDefault) { this.loadLocale(localizationDefault.getName(), localizationDefault.getDefault()); } /** * Loads a single Localization value<br> * Adds this node to the localization configuration if it wsn't added * * @param path to the value (case insensitive, can not be null) * @param defaultValue for the value */ public void loadLocale(String path, String defaultValue) { path = path.toLowerCase(Locale.ENGLISH); if (!this.localizationconfig.contains(path)) { this.localizationconfig.set(path, defaultValue); } } /** * Tries to find the command configuration for a command * * @param command to find the configuration node for * @return The configuration node, or null if not found */ private ConfigurationNode getCommandNode(String command) { command = command.toLowerCase(Locale.ENGLISH); String fullPath = "commands." + command; if (this.localizationconfig.isNode(fullPath)) { return this.localizationconfig.getNode(fullPath); } else { fullPath = "commands." + command.replace('.', ' '); if (this.localizationconfig.isNode(fullPath)) { return this.localizationconfig.getNode(fullPath); } else { return null; } } } /** * Gets the localized usage for a command * * @param command name (case insensitive) * @return command usage */ public String getCommandUsage(String command) { ConfigurationNode node = getCommandNode(command); final String defValue = "/" + command; if (node == null) { return defValue; } else { return node.get("usage", defValue); } } /** * Gets the localized description for a command * * @param command name (case insensitive) * @return command description */ public String getCommandDescription(String command) { ConfigurationNode node = getCommandNode(command); final String defValue = "No description specified"; if (node == null) { return defValue; } else { return node.get("description", defValue); } } /** * Gets a localization value * * @param path to the localization value (case insensitive, can not be null) * @param arguments to use for the value * @return Localization value */ public String getLocale(String path, String... arguments) { path = path.toLowerCase(Locale.ENGLISH); // First check if the path leads to a node if (this.localizationconfig.isNode(path)) { // Redirect to the proper sub-node // Check recursively if the arguments are contained String newPath = path + ".default"; if (arguments.length > 0) { StringBuilder tmpPathBuilder = new StringBuilder(path); String tmpPath = path; for (int i = 0; i < arguments.length; i++) { tmpPathBuilder.append('.'); if (arguments[i] == null) { tmpPathBuilder.append("null"); } else { tmpPathBuilder.append(arguments[i].toLowerCase(Locale.ENGLISH)); } tmpPath = tmpPathBuilder.toString(); // New argument appended path exists, update the path if (this.localizationconfig.contains(tmpPath)) { newPath = tmpPath; } else { break; } } } // Update path to lead to the new path path = newPath; } // Regular loading going on if (arguments.length > 0) { StringBuilder locale = new StringBuilder(this.localizationconfig.get(path, "")); for (int i = 0; i < arguments.length; i++) { StringUtil.replaceAll(locale, "%" + i + "%", LogicUtil.fixNull(arguments[i], "null")); } return locale.toString(); } else { return this.localizationconfig.get(path, String.class, ""); } } /** * Fired when the Permission nodes have to be created */ public void permissions() { } /** * Fired when the Localization nodes have to be created */ public void localization() { } /** * Gets the disable message shown when this Plugin disables * * @return disable message */ public final String getDisableMessage() { return this.disableMessage; } /** * Sets the disable message shown when this Plugin disables * * @param msg to set to, null to disable the message */ public void setDisableMessage(String msg) { this.disableMessage = msg; } /** * Gets the enable message shown after this Plugin enabled successfully * * @return enable message */ public final String getEnableMessage() { return this.enableMessage; } /** * Sets the enable message shown after this Plugin enabled successfully * * @param msg to set to, null to disable the message */ public void setEnableMessage(String msg) { this.enableMessage = msg; } /** * Gets the minimum BKCommonLib version required for this Plugin to function<br> * Override this and return Common.VERSION as result (compiler will automatically inline this) * * @return Minimum BKCommonLib version number */ public abstract int getMinimumLibVersion(); /** * Handles a possible throwable thrown somewhere in the Plugin<br> * If the throwable is too severe, the plugin is automatically disabled<br> * Additional exception types can be handled if needed * * @param reason to throw */ @SuppressWarnings("unchecked") public void handle(Throwable reason) { if (reason instanceof Exception) { StackTraceFilter.SERVER.print(reason); } else if (reason instanceof OutOfMemoryError) { log(Level.SEVERE, "The server is running out of memory! Do something!"); } else { String pluginCause = getName(); if (CommonUtil.isInstance(reason, NoClassDefFoundError.class, NoSuchMethodError.class, NoSuchFieldError.class, IllegalAccessError.class)) { String fixedReason = StringUtil.trimStart(reason.getMessage(), "tried to access "); String path = StringUtil.trimStart(fixedReason, "method ", "field ", "class "); if (path.startsWith(Common.NMS_ROOT)) { log(Level.SEVERE, "This version of the plugin is incompatible with this Minecraft version:"); } else if (path.startsWith(Common.CB_ROOT)) { log(Level.SEVERE, "This version of the plugin is incompatible with this CraftBukkit implementation:"); } else if (path.startsWith("org.bukkit")) { log(Level.SEVERE, "This version of the plugin is incompatible with the current Bukkit API:"); } else { final Plugin plugin = CommonUtil.getPluginByClass(path); if (plugin == this) { if (reason instanceof NoClassDefFoundError) { log(Level.WARNING, "Class is missing (plugin was hot-swapped?): " + reason.getMessage()); return; } else { log(Level.SEVERE, "Encountered a compiler error"); } } else { // Obtain the type of happening final String type; if (reason instanceof IllegalAccessError) { if (fixedReason.startsWith("class ")) { type = "Class is inaccessible in"; } else if (fixedReason.startsWith("method ")) { type = "Method is inaccessible in"; } else if (fixedReason.startsWith("field ")) { type = "Field is inaccessible in"; } else { type = "Something is inaccessible in"; } } else if (reason instanceof NoClassDefFoundError) { type = "Class is missing from"; } else if (reason instanceof NoSuchMethodError) { type = "Method is missing from"; } else if (reason instanceof NoSuchFieldError) { type = "Field is missing from"; } else { type = "Something is missing from"; } // Log the message if (plugin == null) { log(Level.SEVERE, type + " a dependency of this plugin"); // Add all dependencies of this plugin to the cause LinkedHashSet<String> dep = new LinkedHashSet<String>(); dep.add(this.getName()); dep.addAll(LogicUtil.fixNull(this.getDescription().getDepend(), Collections.EMPTY_LIST)); dep.addAll(LogicUtil.fixNull(this.getDescription().getSoftDepend(), Collections.EMPTY_LIST)); pluginCause = StringUtil.combineNames(dep); } else { pluginCause = getName() + " and " + plugin.getName(); log(Level.SEVERE, type + " dependency '" + plugin.getName() + "'"); } } } } else { log(Level.SEVERE, "Encountered a critical error"); } log(Level.SEVERE, "Please, check for an updated version of " + pluginCause + " before reporting this bug!"); StackTraceFilter.SERVER.print(reason); } } private static void setPermissions(ConfigurationNode node) { for (ConfigurationNode subNode : node.getNodes()) { setPermissions(subNode); } PermissionDefault def = node.get("default", PermissionDefault.class); String desc = node.get("description", String.class); if (def != null || desc != null) { Permission permission = getPermission(node.getPath().toLowerCase()); if (def != null) { permission.setDefault(def); } if (desc != null) { permission.setDescription(desc); } } } /** * Gets the Metrics instance for this Plugin, which is used to send statistics to * <a href="http://mcstats.org/">http://mcstats.org/</a><br> * To make use of this functionality, first add the following line to the <b>plugin.yml</b>:<br> * <pre>metrics: true</pre> * * To avoid issues, call {@link #hasMetrics()} before using this method to check whether Metrics is available. * * @return the Metrics instance used * @throws RuntimeException if no metrics is available */ public Metrics getMetrics() { if (metrics == null) { throw new RuntimeException("Metrics is not enabled or failed to initialize for this Plugin."); } return metrics; } /** * Checks whether Metrics is available for this Plugin. Always call this method * before using {@link #getMetrics()} - initialization of Metrics could have failed! * * @return True if Metrics is available, False if not */ public boolean hasMetrics() { return metrics != null; } @Override @SuppressWarnings("unchecked") public final void onEnable() { // Shortcut to avoid unneeded initialization: calling enable will result in BKCommonLib disabling if (!Common.IS_COMPATIBLE && this instanceof CommonPlugin) { this.enable(); return; } // First of all, check that all dependencies are properly enabled for (String dep : LogicUtil.fixNull(getDescription().getDepend(), (List<String>) Collections.EMPTY_LIST)) { if (!Bukkit.getPluginManager().isPluginEnabled(dep)) { log(Level.SEVERE, "Could not enable '" + getName() + " v" + getVersion() + "' because dependency '" + dep + "' failed to enable!"); log(Level.SEVERE, "Perhaps the dependency has to be updated? Please check the log for any errors related to " + dep); Bukkit.getPluginManager().disablePlugin(this); return; } } long startTime = System.currentTimeMillis(); if (this.getMinimumLibVersion() > Common.VERSION) { log(Level.SEVERE, "Requires a newer BKCommonLib version, please update BKCommonLib to the latest version!"); log(Level.SEVERE, "Verify that there is only one BKCommonLib.jar in the plugins folder before retrying"); Bukkit.getPluginManager().disablePlugin(this); return; } this.setDisableMessage(this.getName() + " disabled!"); // Load permission configuration this.permissionconfig = new FileConfiguration(this, "PermissionDefaults.yml"); // load if (this.permissionconfig.exists()) { this.loadPermissions(); } // header this.permissionconfig.setHeader("Below are the default permissions set for plugin '" + this.getName() + "'."); this.permissionconfig.addHeader("These permissions are ignored if the permission is set for a group or player."); this.permissionconfig.addHeader("Use the defaults as a base to keep the permissions file small"); this.permissionconfig.addHeader("Need help with this file? Please visit:"); this.permissionconfig.addHeader("http://dev.bukkit.org/server-mods/bkcommonlib/pages/general/permission-defaults/"); // Load localization configuration this.localizationconfig = new FileConfiguration(this, "Localization.yml"); // load if (this.localizationconfig.exists()) { this.loadLocalization(); } // header this.localizationconfig.setHeader("Below are the localization nodes set for plugin '" + this.getName() + "'."); this.localizationconfig.addHeader("For colors, use the & character followed up by 0 - F"); this.localizationconfig.addHeader("Need help with this file? Please visit:"); this.localizationconfig.addHeader("http://dev.bukkit.org/server-mods/bkcommonlib/pages/general/localization/"); // Load plugin.yml configuration try { this.pluginYaml.loadFromStream(getResource("plugin.yml")); } catch (Exception ex) { Bukkit.getLogger().log(Level.SEVERE, "[Configuration] An error occured while loading plugin.yml resource for plugin " + getName() + ":"); } // Load all the commands for this Plugin Map<String, Map<String, Object>> commands = this.getDescription().getCommands(); if (commands != null && PluginDescriptionFileRef.commands.isValid()) { // Prepare commands localization node ConfigurationNode commandsNode = getLocalizationNode("commands"); // Create a new modifiable commands map to replace with commands = new HashMap<String, Map<String, Object>>(commands); for (Entry<String, Map<String, Object>> commandEntry : commands.entrySet()) { ConfigurationNode node = commandsNode.getNode(commandEntry.getKey()); // Transfer description and usage Map<String, Object> data = new HashMap<String, Object>(commandEntry.getValue()); node.shareWith(data, "description", "No description specified"); node.shareWith(data, "usage", "/" + commandEntry.getKey()); commandEntry.setValue(Collections.unmodifiableMap(data)); } // Set the new commands map using reflection PluginDescriptionFileRef.commands.set(this.getDescription(), Collections.unmodifiableMap(commands)); } // ==== Permissions ==== this.permissions(); // Load all nodes from the permissions config setPermissions(this.permissionconfig); if (!this.permissionconfig.isEmpty()) { this.savePermissions(); } // ==== Localization ==== this.localization(); if (!this.localizationconfig.isEmpty()) { this.saveLocalization(); } // ==== Enabling ==== try { // Metrics if (this.pluginYaml.get("metrics", false)) { // Send anonymous statistics to mcstats.org try { this.metrics = new Metrics(this); } catch (IOException ex) { log(Level.SEVERE, "Failed to initialize metrics for " + getName()); CommonUtil.printFilteredStackTrace(ex); } } this.wasDisableRequested = false; this.enable(); if (this.wasDisableRequested) { // Plugin was disabled again while enabling return; } // Start Metrics if enabled if (metrics != null) { metrics.start(); } // Done, this plugin is enabled this.enabled = true; } catch (Throwable t) { log(Level.SEVERE, "An error occurred while enabling, the plugin will be disabled:"); handle(t); Bukkit.getPluginManager().disablePlugin(this); return; } // update dependencies CommonPlugin.getInstance().plugins.add(this); for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) { if (plugin.isEnabled()) { this.updateDependency(plugin, plugin.getName(), true); } } // Enable messages if (this.enableMessage != null) { log(Level.INFO, this.enableMessage); } Bukkit.getLogger().log(Level.INFO, this.getName() + " version " + this.getVersion() + " enabled! (" + MathUtil.round(0.001 * (System.currentTimeMillis() - startTime), 3) + "s)"); } @Override public final void onDisable() { // are there any plugins that depend on me? for (Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) { if (plugin.isEnabled() && CommonUtil.isDepending(plugin, this)) { Bukkit.getServer().getPluginManager().disablePlugin(plugin); } } this.wasDisableRequested = true; boolean doDisableMessage = this.disableMessage != null; if (this.enabled) { // Try to disable the plugin try { this.disable(); } catch (Throwable t) { log(Level.SEVERE, "An error occurred while disabling:"); StackTraceFilter.SERVER.print(t); doDisableMessage = false; } // Remove references to the plugin - it is disabled now this.enabled = false; if (CommonPlugin.hasInstance()) { CommonPlugin.getInstance().plugins.remove(this); } } // Disable Metrics if enabled if (metrics != null) { metrics.stop(); metrics = null; } // If specified to do so, a disable message is shown if (doDisableMessage) { Bukkit.getLogger().log(Level.INFO, this.disableMessage); } } @Override public final boolean onCommand(CommandSender sender, org.bukkit.command.Command cmd, String command, String[] args) { try { String[] fixedArgs = StringUtil.convertArgs(args); // Default commands for all plugins if (fixedArgs.length >= 1 && LogicUtil.contains(fixedArgs[0].toLowerCase(Locale.ENGLISH), "version", "ver")) { sender.sendMessage(ChatColor.GREEN + this.getName() + " v" + this.getVersion() + " using BKCommonLib v" + CommonPlugin.getInstance().getVersion()); } // Handle regularly if (command(sender, command, fixedArgs)) { return true; } sender.sendMessage(ChatColor.RED + "Unknown command, for help use /help " + command); } catch (NoPermissionException ex) { if (sender instanceof Player) { sender.sendMessage(ChatColor.RED + "You do not have permission to use this command!"); } else { sender.sendMessage("This command is only for players!"); } } catch (Throwable t) { StringBuilder msg = new StringBuilder("Unhandled exception executing command '"); msg.append(command).append("' in plugin ").append(this.getName()).append(" v").append(this.getVersion()); Bukkit.getLogger().log(Level.SEVERE, msg.toString()); t.printStackTrace(); sender.sendMessage(ChatColor.RED + "An internal error occured while executing this command"); } return true; } /** * Called when this plugin is being enabled */ public abstract void enable(); /** * Called when this plugin is being disabled */ public abstract void disable(); /** * Handles a command * * @param sender of the command * @param command name * @param args of the command * @return True if handled, False if not */ public abstract boolean command(CommandSender sender, String command, String[] args); public final void loadLocalization() { this.localizationconfig.load(); } /** * Obtains a configuration instance managed by this PluginBase containing the contents of the plugin.yml * * @return plugin.yml configuration */ public final BasicConfiguration getPluginYaml() { return this.pluginYaml; } public final void saveLocalization() { this.localizationconfig.save(); } public final void loadPermissions() { this.permissionconfig.load(); } public final void savePermissions() { this.permissionconfig.save(); } /** * Called when a plugin is enabled or disabled * * @param plugin that got enabled or disabled * @param pluginName of the plugin * @param enabled state, True if enabled, False if disabled */ public void updateDependency(Plugin plugin, String pluginName, boolean enabled) { }; }