diff --git a/ProtocolLib/.classpath b/ProtocolLib/.classpath index 3e0a6999..e842aee4 100644 --- a/ProtocolLib/.classpath +++ b/ProtocolLib/.classpath @@ -1,6 +1,6 @@ - + @@ -13,6 +13,11 @@ + + + + + diff --git a/ProtocolLib/.settings/org.eclipse.core.resources.prefs b/ProtocolLib/.settings/org.eclipse.core.resources.prefs index 6e796e59..78478a74 100644 --- a/ProtocolLib/.settings/org.eclipse.core.resources.prefs +++ b/ProtocolLib/.settings/org.eclipse.core.resources.prefs @@ -1,4 +1,5 @@ eclipse.preferences.version=1 encoding//src/main/java=cp1252 +encoding//src/main/resources=cp1252 encoding//src/test/java=cp1252 encoding/=cp1252 diff --git a/ProtocolLib/dependency-reduced-pom.xml b/ProtocolLib/dependency-reduced-pom.xml index 6b5086a3..59e0de0c 100644 --- a/ProtocolLib/dependency-reduced-pom.xml +++ b/ProtocolLib/dependency-reduced-pom.xml @@ -38,7 +38,7 @@ clean install - src/main/java + src/main/resources **/*.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java new file mode 100644 index 00000000..510842a2 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -0,0 +1,325 @@ +package com.comphenix.protocol; + +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.concurrency.AbstractIntervalTree; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.google.common.collect.DiscreteDomains; +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * Handles the "packet" debug command. + * + * @author Kristian + */ +class CommandPacket implements CommandExecutor { + private interface DetailedPacketListener extends PacketListener { + /** + * Determine whether or not the given packet listener is detailed or not. + * @return TRUE if it is detailed, FALSE otherwise. + */ + public boolean isDetailed(); + } + + private enum SubCommand { + ADD, REMOVE; + } + + /** + * Name of this command. + */ + public static final String NAME = "packet"; + + private Plugin plugin; + private Logger logger; + private ProtocolManager manager; + + // Registered packet listeners + private AbstractIntervalTree clientListeners = createTree(ConnectionSide.CLIENT_SIDE); + private AbstractIntervalTree serverListeners = createTree(ConnectionSide.SERVER_SIDE); + + public CommandPacket(Plugin plugin, Logger logger, ProtocolManager manager) { + this.plugin = plugin; + this.logger = logger; + this.manager = manager; + } + + /** + * Construct a packet listener interval tree. + * @return Construct the tree. + */ + private AbstractIntervalTree createTree(final ConnectionSide side) { + return new AbstractIntervalTree() { + @Override + protected Integer decrementKey(Integer key) { + return key != null ? key - 1 : null; + } + + @Override + protected Integer incrementKey(Integer key) { + return key != null ? key + 1 : null; + } + + @Override + protected void onEntryAdded(Entry added) { + // Ensure that the starting ID and the ending ID is correct + // This is necessary because the interval tree may change the range. + if (added != null) { + Range key = added.getKey(); + DetailedPacketListener listener = added.getValue(); + DetailedPacketListener corrected = createPacketListener( + side, key.lowerEndpoint(), key.upperEndpoint(), listener.isDetailed()); + + added.setValue(corrected); + + if (corrected != null) { + manager.addPacketListener(corrected); + } else { + // Never mind + remove(key.lowerEndpoint(), key.upperEndpoint()); + } + } + } + + @Override + protected void onEntryRemoved(Entry removed) { + // Remove the listener + if (removed != null) { + DetailedPacketListener listener = removed.getValue(); + + if (listener != null) { + manager.removePacketListener(listener); + } + } + } + }; + } + + /* + * Description: Adds or removes a simple packet listener. + Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] + */ + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Make sure we're dealing with the correct command + if (!command.getName().equalsIgnoreCase(NAME)) + return false; + + // We need at least one argument + if (args != null && args.length > 0) { + try { + SubCommand subCommand = parseCommand(args, 0); + ConnectionSide side = parseSide(args, 1, ConnectionSide.BOTH); + + int idStart = parseInteger(args, 2, 0); + int idStop = parseInteger(args, 3, 255); + + // Make sure the packet IDs are valid + if (idStart < 0 || idStart > 255) + throw new IllegalAccessError("The starting packet ID must be within 0 - 255."); + if (idStop < 0 || idStop > 255) + throw new IllegalAccessError("The stop packet ID must be within 0 - 255."); + + // Special case. If stop is not set, but start is set, use a interval size of 1. + if (args.length == 3) + idStop = idStart + 1; + + boolean detailed = parseBoolean(args, 4, false); + + // Perform command + if (subCommand == SubCommand.ADD) + addPacketListeners(side, idStart, idStop, detailed); + else + removePacketListeners(side, idStart, idStop, detailed); + + } catch (NumberFormatException e) { + sender.sendMessage(ChatColor.DARK_RED + "Cannot parse number: " + e.getMessage()); + } catch (IllegalArgumentException e) { + sender.sendMessage(ChatColor.DARK_RED + e.getMessage()); + } + } + + return false; + } + + private Set getValidPackets(ConnectionSide side) throws FieldAccessException { + if (side.isForClient()) + return Packets.Client.getSupported(); + else if (side.isForServer()) + return Packets.Server.getSupported(); + else + throw new IllegalArgumentException("Illegal side: " + side); + } + + public DetailedPacketListener createPacketListener(final ConnectionSide side, int idStart, int idStop, final boolean detailed) { + + Set range = Ranges.closed(idStart, idStop).asSet(DiscreteDomains.integers()); + Set packets; + + try { + // Only use supported packet IDs + packets = getValidPackets(side); + packets.retainAll(range); + + } catch (FieldAccessException e) { + // Don't filter anything then + packets = range; + } + + // Ignore empty sets + if (packets.isEmpty()) + return null; + + // Create the listener we will be using + final ListeningWhitelist whitelist = new ListeningWhitelist(ListenerPriority.MONITOR, packets, GamePhase.BOTH); + + return new DetailedPacketListener() { + @Override + public void onPacketSending(PacketEvent event) { + if (side.isForServer()) { + printInformation(event); + } + } + + @Override + public void onPacketReceiving(PacketEvent event) { + if (side.isForClient()) { + printInformation(event); + } + } + + private void printInformation(PacketEvent event) { + String verb = side.isForClient() ? "Received" : "Sent"; + String shortDescription = String.format( + "%s packet %s (%s)", + verb, + event.getPacketID(), + Packets.getDeclaredName(event.getPacketID()) + ); + + // Detailed will print the packet's content too + if (detailed) { + logger.info(shortDescription + ":\n" + + ToStringBuilder.reflectionToString(event.getPacket().getHandle(), ToStringStyle.MULTI_LINE_STYLE) + ); + } else { + logger.info(shortDescription + "."); + } + } + + @Override + public ListeningWhitelist getSendingWhitelist() { + return side.isForServer() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public ListeningWhitelist getReceivingWhitelist() { + return side.isForClient() ? whitelist : ListeningWhitelist.EMPTY_WHITELIST; + } + + @Override + public Plugin getPlugin() { + return plugin; + } + + @Override + public boolean isDetailed() { + return detailed; + } + }; + } + + public void addPacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { + DetailedPacketListener listener = createPacketListener(side, idStart, idStop, detailed); + + // The trees will manage the listeners for us + if (listener != null) + getListenerTree(side).put(idStart, idStop, listener); + else + throw new IllegalArgumentException("No packets found in the range " + idStart + " - " + idStop + "."); + } + + public void removePacketListeners(ConnectionSide side, int idStart, int idStop, boolean detailed) { + // The interval tree will automatically remove the listeners for us + getListenerTree(side).remove(idStart, idStop); + } + + private AbstractIntervalTree getListenerTree(ConnectionSide side) { + if (side.isForClient()) + return clientListeners; + else if (side.isForServer()) + return serverListeners; + else + throw new IllegalArgumentException("Not a legal connection side."); + } + + private SubCommand parseCommand(String[] args, int index) { + String text = args[index].toLowerCase(); + + // Parse this too + if ("add".startsWith(text)) + return SubCommand.ADD; + else if ("remove".startsWith(text)) + return SubCommand.REMOVE; + else + throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove."); + } + + private ConnectionSide parseSide(String[] args, int index, ConnectionSide defaultValue) { + if (index < args.length) { + String text = args[index].toLowerCase(); + + // Parse the side gracefully + if ("both".startsWith(text)) + return ConnectionSide.BOTH; + else if ("client".startsWith(text)) + return ConnectionSide.CLIENT_SIDE; + else if ("server".startsWith(text)) + return ConnectionSide.SERVER_SIDE; + else + throw new IllegalArgumentException(text + " is not a connection side."); + + } else { + return defaultValue; + } + } + + // Parse a boolean + private boolean parseBoolean(String[] args, int index, boolean defaultValue) { + if (index < args.length) { + return Boolean.parseBoolean(args[index]); + } else { + return defaultValue; + } + } + + // And an integer + private int parseInteger(String[] args, int index, int defaultValue) { + if (index < args.length) { + return Integer.parseInt(args[index]); + } else { + return defaultValue; + } + } + + public void cleanupAll() { + clientListeners.clear(); + serverListeners.clear(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java new file mode 100644 index 00000000..e4d504d7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandProtocol.java @@ -0,0 +1,84 @@ +package com.comphenix.protocol; + +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.metrics.Updater; +import com.comphenix.protocol.metrics.Updater.UpdateResult; +import com.comphenix.protocol.metrics.Updater.UpdateType; + +/** + * Handles the "protocol" administration command. + * + * @author Kristian + */ +class CommandProtocol implements CommandExecutor { + /** + * Name of this command. + */ + public static final String NAME = "protocol"; + + private Plugin plugin; + private Updater updater; + + public CommandProtocol(Plugin plugin, Updater updater) { + this.plugin = plugin; + this.updater = updater; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + // Make sure we're dealing with the correct command + if (!command.getName().equalsIgnoreCase(NAME)) + return false; + + // We need one argument (the sub-command) + if (args != null && args.length == 1) { + String subCommand = args[0]; + + // Only return TRUE if we executed the correct command + if (subCommand.equalsIgnoreCase("config")) + reloadConfiguration(sender); + else if (subCommand.equalsIgnoreCase("check")) + checkVersion(sender); + else if (subCommand.equalsIgnoreCase("update")) + updateVersion(sender); + else + return false; + return true; + } + + return false; + } + + public void checkVersion(final CommandSender sender) { + // Perform on an async thread + plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() { + @Override + public void run() { + UpdateResult result = updater.update(UpdateType.NO_DOWNLOAD, true); + sender.sendMessage(ChatColor.DARK_BLUE + "Version check: " + result.toString()); + } + }); + } + + public void updateVersion(final CommandSender sender) { + // Perform on an async thread + plugin.getServer().getScheduler().scheduleAsyncDelayedTask(plugin, new Runnable() { + @Override + public void run() { + UpdateResult result = updater.update(UpdateType.DEFAULT, true); + sender.sendMessage(ChatColor.DARK_BLUE + "Update: " + result.toString()); + } + }); + } + + public void reloadConfiguration(CommandSender sender) { + plugin.saveConfig(); + plugin.reloadConfig(); + sender.sendMessage(ChatColor.DARK_BLUE + "Reloaded configuration!"); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java new file mode 100644 index 00000000..516db0fc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -0,0 +1,135 @@ +package com.comphenix.protocol; + +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.plugin.Plugin; + +/** + * Represents the configuration of ProtocolLib. + * + * @author Kristian + */ +class ProtocolConfig { + + private static final String SECTION_GLOBAL = "global"; + private static final String SECTION_AUTOUPDATER = "auto updater"; + + private static final String UPDATER_NOTIFY = "notify"; + private static final String UPDATER_DOWNLAD = "download"; + private static final String UPDATER_DELAY = "delay"; + private static final String UPDATER_LAST_TIME = "last"; + + // Defaults + private static final long DEFAULT_UPDATER_DELAY = 60; + + private Plugin plugin; + private Configuration config; + + private ConfigurationSection global; + private ConfigurationSection updater; + + public ProtocolConfig(Plugin plugin) { + this(plugin, plugin.getConfig()); + } + + public ProtocolConfig(Plugin plugin, Configuration config) { + this.config = config; + loadSections(true); + } + + /** + * Load data sections. + * @param copyDefaults - whether or not to copy configuration defaults. + */ + private void loadSections(boolean copyDefaults) { + if (config != null) { + global = config.getConfigurationSection(SECTION_GLOBAL); + } + if (global != null) { + updater = global.getConfigurationSection(SECTION_AUTOUPDATER); + } + + // Automatically copy defaults + if (copyDefaults && (global == null || updater == null)) { + config.options().copyDefaults(true); + loadSections(false); + } + } + + /** + * Retrieve whether or not ProtocolLib should determine if a new version has been released. + * @return TRUE if it should do this automatically, FALSE otherwise. + */ + public boolean isAutoNotify() { + return updater.getBoolean(UPDATER_NOTIFY, true); + } + + /** + * Set whether or not ProtocolLib should determine if a new version has been released. + * @param value - TRUE to do this automatically, FALSE otherwise. + */ + public void setAutoNotify(boolean value) { + updater.set(UPDATER_NOTIFY, value); + } + + /** + * Retrieve whether or not ProtocolLib should automatically download the new version. + * @return TRUE if it should, FALSE otherwise. + */ + public boolean isAutoDownload() { + return updater != null && updater.getBoolean(UPDATER_DOWNLAD, true); + } + + /** + * Set whether or not ProtocolLib should automatically download the new version. + * @param value - TRUE if it should. FALSE otherwise. + */ + public void setAutoDownload(boolean value) { + updater.set(UPDATER_DOWNLAD, value); + } + + /** + * Retrieve the amount of time to wait until checking for a new update. + * @return The amount of time to wait. + */ + public long getAutoDelay() { + // Note that the delay must be greater than 59 seconds + return Math.max(updater.getInt(UPDATER_DELAY, 0), DEFAULT_UPDATER_DELAY); + } + + /** + * Set the amount of time to wait until checking for a new update. + *

+ * This time must be greater than 59 seconds. + * @param delaySeconds - the amount of time to wait. + */ + public void setAutoDelay(long delaySeconds) { + // Silently fix the delay + if (delaySeconds < DEFAULT_UPDATER_DELAY) + delaySeconds = DEFAULT_UPDATER_DELAY; + updater.set(UPDATER_DELAY, delaySeconds); + } + + /** + * Retrieve the last time we updated, in seconds since 1970.01.01 00:00. + * @return Last update time. + */ + public long getAutoLastTime() { + return updater.getLong(UPDATER_LAST_TIME, 0); + } + + /** + * Set the last time we updated, in seconds since 1970.01.01 00:00. + * @param lastTimeSeconds - new last update time. + */ + public void setAutoLastTime(long lastTimeSeconds) { + updater.set(UPDATER_LAST_TIME, lastTimeSeconds); + } + + /** + * Save the current configuration file. + */ + public void saveAll() { + plugin.saveConfig(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index c17b84d9..e0acc802 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -27,12 +27,10 @@ import org.bukkit.plugin.java.JavaPlugin; import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.error.DetailedErrorReporter; import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.ConnectionSide; -import com.comphenix.protocol.events.MonitorAdapter; -import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.DelayedSingleTask; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.metrics.Statistics; +import com.comphenix.protocol.metrics.Updater; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; /** @@ -42,12 +40,11 @@ import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; */ public class ProtocolLibrary extends JavaPlugin { + private static final long MILLI_PER_SECOND = 1000; + // There should only be one protocol manager, so we'll make it static private static PacketFilterManager protocolManager; - // Information logger - private Logger logger; - // Error reporter private ErrorReporter reporter; @@ -66,36 +63,66 @@ public class ProtocolLibrary extends JavaPlugin { // Used to unhook players after a delay private DelayedSingleTask unhookTask; - // Used for debugging - private boolean debugListener; + // Settings/options + private ProtocolConfig config; + + // Updater + private Updater updater; + + // Commands + private CommandProtocol commandProtocol; + private CommandPacket commandPacket; @Override public void onLoad() { // Add global parameters DetailedErrorReporter reporter = new DetailedErrorReporter(); + // Load configuration + updater = new Updater(this, "protocollib", getFile(), "protocol.info"); + config = new ProtocolConfig(this); + try { - logger = getLoggerSafely(); unhookTask = new DelayedSingleTask(this); protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, reporter); reporter.addGlobalParameter("manager", protocolManager); + // Initialize command handlers + commandProtocol = new CommandProtocol(this, updater); + commandPacket = new CommandPacket(this, getLoggerSafely(), protocolManager); + } catch (Throwable e) { reporter.reportDetailed(this, "Cannot load ProtocolLib.", e, protocolManager); + disablePlugin(); } } + @Override + public void reloadConfig() { + super.reloadConfig(); + // Reload configuration + config = new ProtocolConfig(this); + } + @Override public void onEnable() { try { Server server = getServer(); PluginManager manager = server.getPluginManager(); - + + // Don't do anything else! + if (manager == null) + return; + // Initialize background compiler if (backgroundCompiler == null) { backgroundCompiler = new BackgroundCompiler(getClassLoader()); BackgroundCompiler.setInstance(backgroundCompiler); } + + // Set up command handlers + getCommand(CommandProtocol.NAME).setExecutor(commandProtocol); + getCommand(CommandPacket.NAME).setExecutor(commandPacket); // Notify server managers of incompatible plugins checkForIncompatibility(manager); @@ -104,8 +131,8 @@ public class ProtocolLibrary extends JavaPlugin { protocolManager.registerEvents(manager, this); // Worker that ensures that async packets are eventually sent + // It also performs the update check. createAsyncTask(server); - //toggleDebugListener(); } catch (Throwable e) { reporter.reportDetailed(this, "Cannot enable ProtocolLib.", e); @@ -122,39 +149,6 @@ public class ProtocolLibrary extends JavaPlugin { reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc); } } - - /** - * Toggle a listener that prints every sent and received packet. - */ - void toggleDebugListener() { - - if (debugListener) { - protocolManager.removePacketListeners(this); - } else { - // DEBUG DEBUG - protocolManager.addPacketListener(new MonitorAdapter(this, ConnectionSide.BOTH, logger) { - @Override - public void onPacketReceiving(PacketEvent event) { - Object handle = event.getPacket().getHandle(); - - logger.info(String.format( - "RECEIVING %s@%s from %s.", - handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName() - )); - }; - @Override - public void onPacketSending(PacketEvent event) { - Object handle = event.getPacket().getHandle(); - - logger.info(String.format( - "SENDING %s@%s from %s.", - handle.getClass().getSimpleName(), handle.hashCode(), event.getPlayer().getName() - )); - } - }); - } - debugListener = !debugListener; - } /** * Disable the current plugin. @@ -176,6 +170,9 @@ public class ProtocolLibrary extends JavaPlugin { // We KNOW we're on the main thread at the moment manager.sendProcessedPackets(tickCounter++, true); + + // Check for updates too + checkUpdates(); } }, ASYNC_PACKET_DELAY, ASYNC_PACKET_DELAY); @@ -186,6 +183,24 @@ public class ProtocolLibrary extends JavaPlugin { } } + private void checkUpdates() { + // Ignore milliseconds - it's pointless + long currentTime = System.currentTimeMillis() / MILLI_PER_SECOND; + + // Should we update? + if (currentTime < config.getAutoLastTime() + config.getAutoDelay()) { + // Great. Save this check. + config.setAutoLastTime(currentTime); + config.saveAll(); + + // Initiate the update from the console + if (config.isAutoDownload()) + commandProtocol.updateVersion(getServer().getConsoleSender()); + else if (config.isAutoNotify()) + commandProtocol.checkVersion(getServer().getConsoleSender()); + } + } + private void checkForIncompatibility(PluginManager manager) { // Plugin authors: Notify me to remove these String[] incompatiblePlugins = {}; @@ -223,6 +238,20 @@ public class ProtocolLibrary extends JavaPlugin { cleanup.resetAll(); } + // Get the Bukkit logger first, before we try to create our own + private Logger getLoggerSafely() { + Logger log = null; + + try { + log = getLogger(); + } catch (Throwable e) { } + + // Use the default logger instead + if (log == null) + log = Logger.getLogger("Minecraft"); + return log; + } + /** * Retrieves the packet protocol manager. * @return Packet protocol manager, or NULL if it has been disabled. @@ -240,20 +269,4 @@ public class ProtocolLibrary extends JavaPlugin { public Statistics getStatistics() { return statistisc; } - - // Get the Bukkit logger first, before we try to create our own - private Logger getLoggerSafely() { - - Logger log = null; - - try { - log = getLogger(); - } catch (Throwable e) { - // We'll handle it - } - - if (log == null) - log = Logger.getLogger("Minecraft"); - return log; - } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java new file mode 100644 index 00000000..de627887 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java @@ -0,0 +1,362 @@ +package com.comphenix.protocol.concurrency; + +import java.util.HashSet; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.commons.lang.NotImplementedException; + +import com.google.common.collect.Range; +import com.google.common.collect.Ranges; + +/** + * Represents a generic store of intervals and associated values. No two intervals + * can overlap in this representation. + *

+ * Note that this implementation is not thread safe. + * + * @author Kristian + * + * @param - type of the key. Must implement Comparable. + * @param - type of the value to associate. + */ +public abstract class AbstractIntervalTree, TValue> { + + protected enum State { + OPEN, + CLOSE, + BOTH + } + + /** + * Represents a range and a value in this interval tree. + */ + public class Entry implements Map.Entry, TValue> { + private final Range key; + private final TValue value; + + public Entry(Range key, TValue value) { + this.key = key; + this.value = value; + } + + @Override + public Range getKey() { + return key; + } + + @Override + public TValue getValue() { + return value; + } + + @Override + public TValue setValue(TValue value) { + throw new NotImplementedException(); + } + } + + /** + * Represents a single end point (open, close or both) of a range. + */ + protected class EndPoint { + + // Whether or not the end-point is opening a range, closing a range or both. + public State state; + + // The value this range contains + public TValue value; + + public EndPoint(State state, TValue value) { + this.state = state; + this.value = value; + } + } + + // To quickly look up ranges we'll index them by endpoints + protected NavigableMap bounds = new TreeMap(); + + /** + * Removes every interval that intersects with the given range. + * @param lowerBound - lowest value to remove. + * @param upperBound - highest value to remove. + */ + public void remove(TKey lowerBound, TKey upperBound) { + remove(lowerBound, upperBound, false); + } + + /** + * Removes every interval that intersects with the given range. + * @param lowerBound - lowest value to remove. + * @param upperBound - highest value to remove. + * @param preserveOutside - whether or not to preserve the intervals that are partially outside. + */ + public void remove(TKey lowerBound, TKey upperBound, boolean preserveDifference) { + checkBounds(lowerBound, upperBound); + NavigableMap range = bounds.subMap(lowerBound, true, upperBound, true); + + boolean emptyRange = range.isEmpty(); + TKey first = emptyRange ? range.firstKey() : null; + TKey last = emptyRange ? range.lastKey() : null; + + Set resized = new HashSet(); + Set removed = new HashSet(); + + // Remove the previous element too. A close end-point must be preceded by an OPEN end-point. + if (first != null && range.get(first).state == State.CLOSE) { + TKey key = bounds.floorKey(first); + EndPoint removedPoint = removeIfNonNull(key); + + // Add the interval back + if (removedPoint != null && preserveDifference) { + resized.add(putUnsafe(key, decrementKey(lowerBound), removedPoint.value)); + } + } + + // Get the closing element too. + if (last != null && range.get(last).state == State.OPEN) { + TKey key = bounds.ceilingKey(last); + EndPoint removedPoint = removeIfNonNull(key); + + if (removedPoint != null && preserveDifference) { + resized.add(putUnsafe(incrementKey(upperBound), key, removedPoint.value)); + } + } + + // Get the removed entries too + getEntries(removed, range); + invokeEntryRemoved(removed); + + if (preserveDifference) { + invokeEntryRemoved(resized); + invokeEntryAdded(resized); + } + + // Remove the range as well + range.clear(); + } + + // Helper + private EndPoint removeIfNonNull(TKey key) { + if (key != null) { + return bounds.remove(key); + } else { + return null; + } + } + + // Adds a given end point + protected EndPoint addEndPoint(TKey key, TValue value, State state) { + EndPoint endPoint = bounds.get(key); + + if (endPoint != null) { + endPoint.state = State.BOTH; + } else { + endPoint = bounds.put(key, new EndPoint(state, value)); + } + return endPoint; + } + + /** + * Associates a given interval of keys with a certain value. Any previous + * association will be overwritten in the given interval. + *

+ * Overlapping intervals are not permitted. A key can only be associated with a single value. + * + * @param lowerBound - the minimum key (inclusive). + * @param upperBound - the maximum key (inclusive). + * @param value - the value, or NULL to reset this range. + */ + public void put(TKey lowerBound, TKey upperBound, TValue value) { + // While we don't permit overlapping intervals, we'll still allow overwriting existing intervals. + remove(lowerBound, upperBound, true); + invokeEntryAdded(putUnsafe(lowerBound, upperBound, value)); + } + + /** + * Associates a given interval without performing any interval checks. + * @param lowerBound - the minimum key (inclusive). + * @param upperBound - the maximum key (inclusive). + * @param value - the value, or NULL to reset the range. + */ + private Entry putUnsafe(TKey lowerBound, TKey upperBound, TValue value) { + // OK. Add the end points now + if (value != null) { + addEndPoint(lowerBound, value, State.OPEN); + addEndPoint(upperBound, value, State.CLOSE); + + Range range = Ranges.closed(lowerBound, upperBound); + return new Entry(range, value); + } else { + return null; + } + } + + /** + * Used to verify the validity of the given interval. + * @param lowerBound - lower bound (inclusive). + * @param upperBound - upper bound (inclusive). + */ + private void checkBounds(TKey lowerBound, TKey upperBound) { + if (lowerBound == null) + throw new IllegalAccessError("lowerbound cannot be NULL."); + if (upperBound == null) + throw new IllegalAccessError("upperBound cannot be NULL."); + if (upperBound.compareTo(lowerBound) < 0) + throw new IllegalArgumentException("upperBound cannot be less than lowerBound."); + } + + /** + * Determines if the given key is within an interval. + * @param key - key to check. + * @return TRUE if the given key is within an interval in this tree, FALSE otherwise. + */ + public boolean containsKey(TKey key) { + return getEndPoint(key) != null; + } + + /** + * Enumerates over every range in this interval tree. + * @return Number of ranges. + */ + public Set entrySet() { + // Don't mind the Java noise + Set result = new HashSet(); + getEntries(result, bounds); + return result; + } + + /** + * Remove every interval. + */ + public void clear() { + if (!bounds.isEmpty()) { + remove(bounds.firstKey(), bounds.lastKey()); + } + } + + /** + * Converts a map of end points into a set of entries. + * @param destination - set of entries. + * @param map - a map of end points. + */ + private void getEntries(Set destination, NavigableMap map) { + Map.Entry last = null; + + for (Map.Entry entry : bounds.entrySet()) { + switch (entry.getValue().state) { + case BOTH: + destination.add(new Entry(Ranges.singleton(entry.getKey()), entry.getValue().value)); + break; + case CLOSE: + Range range = Ranges.closed(last.getKey(), entry.getKey()); + destination.add(new Entry(range, entry.getValue().value)); + break; + case OPEN: + // We don't know the full range yet + last = entry; + break; + default: + throw new IllegalStateException("Illegal open/close state detected."); + } + } + } + + /** + * Inserts every range from the given tree into the current tree. + * @param other - the other tree to read from. + */ + public void putAll(AbstractIntervalTree other) { + // Naively copy every range. + for (Entry entry : other.entrySet()) { + put(entry.key.lowerEndpoint(), entry.key.upperEndpoint(), entry.value); + } + } + + /** + * Retrieves the value of the range that matches the given key, or NULL if nothing was found. + * @param key - the level to read for. + * @return The correct amount of experience, or NULL if nothing was recorded. + */ + public TValue get(TKey key) { + EndPoint point = getEndPoint(key); + + if (point != null) + return point.value; + else + return null; + } + + /** + * Get the end-point composite associated with this key. + * @param key - key to search for. + * @return The end point found, or NULL. + */ + protected EndPoint getEndPoint(TKey key) { + EndPoint ends = bounds.get(key); + + if (ends != null) { + // This is a piece of cake + return ends; + } else { + + // We need to determine if the point intersects with a range + TKey left = bounds.floorKey(key); + + // We only need to check to the left + if (left != null && bounds.get(left).state == State.OPEN) { + return bounds.get(left); + } else { + return null; + } + } + } + + private void invokeEntryAdded(Entry added) { + if (added != null) { + onEntryAdded(added); + } + } + + private void invokeEntryAdded(Set added) { + for (Entry entry : added) { + onEntryAdded(entry); + } + } + + private void invokeEntryRemoved(Set removed) { + for (Entry entry : removed) { + onEntryRemoved(entry); + } + } + + // Listeners for added or removed entries + /** + * Invoked when an entry is added. + * @param added - the entry that was added. + */ + protected void onEntryAdded(Entry added) { } + + /** + * Invoked when an entry is removed. + * @param removed - the removed entry. + */ + protected void onEntryRemoved(Entry removed) { } + + // Helpers for decrementing or incrementing key values + /** + * Decrement the given key by one unit. + * @param key - the key that should be decremented. + * @return The new decremented key. + */ + protected abstract TKey decrementKey(TKey key); + + /** + * Increment the given key by one unit. + * @param key - the key that should be incremented. + * @return The new incremented key. + */ + protected abstract TKey incrementKey(TKey key); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java index 13f0a5eb..bceb7dca 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/error/DetailedErrorReporter.java @@ -28,6 +28,9 @@ public class DetailedErrorReporter implements ErrorReporter { public static final String DEFAULT_SUPPORT_URL = "http://dev.bukkit.org/server-mods/protocollib/"; public static final String PLUGIN_NAME = "ProtocolLib"; + // Users that are informed about errors in the chat + public static final String ERROR_PERMISSION = "protocol.info"; + // We don't want to spam the server public static final int DEFAULT_MAX_ERROR_COUNT = 20; @@ -158,8 +161,16 @@ public class DetailedErrorReporter implements ErrorReporter { if (Bukkit.getServer() != null) { writer.println("Server:"); writer.println(addPrefix(Bukkit.getServer().getVersion(), SECOND_LEVEL_PREFIX)); + + // Inform of this occurrence + if (ERROR_PERMISSION != null) { + Bukkit.getServer().broadcast( + String.format("Error %s (%s) occured in %s.", message, error, sender), + ERROR_PERMISSION + ); + } } - + // Make sure it is reported logger.severe(addPrefix(text.toString(), prefix)); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java index 2108c2cd..14eee6d5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java @@ -50,26 +50,31 @@ public class Updater private static final String DBOUrl = "http://dev.bukkit.org/server-mods/"; private static final int BYTE_SIZE = 1024; // Used for downloading files - private Plugin plugin; - private UpdateType type; - private String downloadedVersion; - private String versionTitle; - private String versionLink; - private long totalSize; // Holds the total size of the file - private int sizeLine; // Used for detecting file size - private int multiplier; // Used for determining when to broadcast download updates - private boolean announce; // Whether to announce file downloads - private URL url; // Connecting to RSS + private final Plugin plugin; + private final String slug; - private String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in + private volatile long totalSize; // Holds the total size of the file + private volatile int sizeLine; // Used for detecting file size + private volatile int multiplier; // Used for determining when to broadcast download updates + + private volatile URL url; // Connecting to RSS + + private volatile String updateFolder = YamlConfiguration.loadConfiguration(new File("bukkit.yml")).getString("settings.update-folder"); // The folder that downloads will be placed in // Used for determining the outcome of the update process - private Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; - private String slug; - private File file; + private volatile Updater.UpdateResult result = Updater.UpdateResult.SUCCESS; + + // Whether to announce file downloads + private volatile boolean announce; + + private volatile UpdateType type; + private volatile String downloadedVersion; + private volatile String versionTitle; + private volatile String versionLink; + private volatile File file; // Used to announce progress - private Logger logger; + private volatile Logger logger; // Strings for reading RSS private static final String TITLE = "title"; @@ -84,38 +89,46 @@ public class Updater /** * The updater found an update, and has readied it to be loaded the next time the server restarts/reloads. */ - SUCCESS(1), + SUCCESS(1, "The updater found an update, and has readied it to be loaded the next time the server restarts/reloads."), + /** * The updater did not find an update, and nothing was downloaded. */ - NO_UPDATE(2), + NO_UPDATE(2, "The updater did not find an update, and nothing was downloaded."), + /** * The updater found an update, but was unable to download it. */ - FAIL_DOWNLOAD(3), + FAIL_DOWNLOAD(3, "The updater found an update, but was unable to download it."), + /** * For some reason, the updater was unable to contact dev.bukkit.org to download the file. */ - FAIL_DBO(4), + FAIL_DBO(4, "For some reason, the updater was unable to contact dev.bukkit.org to download the file."), + /** * When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'. */ - FAIL_NOVERSION(5), + FAIL_NOVERSION(5, "When running the version check, the file on DBO did not contain the a version in the format 'vVersion' such as 'v1.0'."), + /** * The slug provided by the plugin running the updater was invalid and doesn't exist on DBO. */ - FAIL_BADSLUG(6), + FAIL_BADSLUG(6, "The slug provided by the plugin running the updater was invalid and doesn't exist on DBO."), + /** * The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded. */ - UPDATE_AVAILABLE(7); + UPDATE_AVAILABLE(7, "The updater found an update, but because of the UpdateType being set to NO_DOWNLOAD, it wasn't downloaded."); private static final Map valueList = new HashMap(); private final int value; + private final String description; - private UpdateResult(int value) + private UpdateResult(int value, String description) { this.value = value; + this.description = description; } public int getValue() @@ -128,6 +141,11 @@ public class Updater return valueList.get(value); } + @Override + public String toString() { + return description; + } + static { for(Updater.UpdateResult result : Updater.UpdateResult.values()) @@ -240,7 +258,7 @@ public class Updater * True if the program should announce the progress of new updates in console * @return The result of the update process. */ - public UpdateResult update(UpdateType type, boolean announce) + public synchronized UpdateResult update(UpdateType type, boolean announce) { this.type = type; this.announce = announce; diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml new file mode 100644 index 00000000..a16abc8f --- /dev/null +++ b/ProtocolLib/src/main/resources/config.yml @@ -0,0 +1,10 @@ +global: + # Settings for the automatic version updater + auto updater: + notify: true + download: true + + # Number of seconds to wait until a new update is downloaded + delay = 43200 # 12 hours + # Last update time + last = 0 \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 2bc5f45d..c684d1d4 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -10,12 +10,12 @@ database: false commands: protocol: description: Performs administrative tasks regarding ProtocolLib. - usage: / [reload|update] + usage: / config|check|update permission: experiencemod.admin permission-message: You don't have packet: description: Adds or removes a simple packet listener. - usage: / [add|remove|clear] [ID start] [ID stop] + usage: / add|remove client|server|both [ID start] [ID stop] [detailed] permission: experiencemod.admin permission-message: You don't have @@ -29,5 +29,5 @@ permissions: description: Able to initiate the update process, and can configure debug mode. default: op protocol.info: - description: Can read update notifications, debug messages and error reports. + description: Can read update notifications and error reports. default: op \ No newline at end of file