diff --git a/ProtocolLib/src/com/comphenix/protocol/AsynchronousManager.java b/ProtocolLib/src/com/comphenix/protocol/AsynchronousManager.java new file mode 100644 index 00000000..eca8466a --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/AsynchronousManager.java @@ -0,0 +1,78 @@ +package com.comphenix.protocol; + +import java.util.Set; +import java.util.logging.Logger; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.async.AsyncListenerHandler; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; + +/** + * Represents a asynchronous packet handler. + * + * @author Kristian + */ +public interface AsynchronousManager { + + /** + * Registers an asynchronous packet handler. + *

+ * To start listening asynchronously, pass the getListenerLoop() runnable to a different thread. + * @param plugin - the plugin that is registering the handler. + * @param listener - the packet listener that will recieve these asynchronous events. + * @return An asynchrouns handler. + */ + public abstract AsyncListenerHandler registerAsyncHandler(PacketListener listener); + + /** + * Unregisters and closes the given asynchronous handler. + * @param handler - asynchronous handler. + */ + public abstract void unregisterAsyncHandler(AsyncListenerHandler handler); + + /** + * Unregisters every asynchronous handler associated with this plugin. + * @param plugin - the original plugin. + */ + public void unregisterAsyncHandlers(Plugin plugin); + + /** + * Retrieves a immutable set containing the ID of the sent server packets that will be + * observed by the asynchronous listeners. + * @return Every filtered server packet. + */ + public abstract Set getSendingFilters(); + + /** + * Retrieves a immutable set containing the ID of the recieved client packets that will be + * observed by the asynchronous listeners. + * @return Every filtered client packet. + */ + public abstract Set getReceivingFilters(); + + /** + * Determine if a given synchronous packet has asynchronous listeners. + * @param packet - packet to test. + * @return TRUE if it does, FALSE otherwise. + */ + public abstract boolean hasAsynchronousListeners(PacketEvent packet); + + /** + * Retrieve the default packet stream. + * @return Default packet stream. + */ + public abstract PacketStream getPacketStream(); + + /** + * Retrieve the default error logger. + * @return Default logger. + */ + public abstract Logger getLogger(); + + /** + * Remove listeners, close threads and transmit every delayed packet. + */ + public abstract void cleanupAll(); +} \ No newline at end of file diff --git a/ProtocolLib/src/com/comphenix/protocol/PacketStream.java b/ProtocolLib/src/com/comphenix/protocol/PacketStream.java new file mode 100644 index 00000000..464f6f5c --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/PacketStream.java @@ -0,0 +1,55 @@ +package com.comphenix.protocol; + +import java.lang.reflect.InvocationTargetException; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketContainer; + +/** + * Represents a object capable of sending or receiving packets. + * + * @author Kristian + */ +public interface PacketStream { + + /** + * Send a packet to the given player. + * @param reciever - the reciever. + * @param packet - packet to send. + * @throws InvocationTargetException - if an error occured when sending the packet. + */ + public void sendServerPacket(Player reciever, PacketContainer packet) + throws InvocationTargetException; + + /** + * Send a packet to the given player. + * @param reciever - the reciever. + * @param packet - packet to send. + * @param filters - whether or not to invoke any packet filters. + * @throws InvocationTargetException - if an error occured when sending the packet. + */ + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) + throws InvocationTargetException; + + /** + * Simulate recieving a certain packet from a given player. + * @param sender - the sender. + * @param packet - the packet that was sent. + * @throws InvocationTargetException If the reflection machinery failed. + * @throws IllegalAccessException If the underlying method caused an error. + */ + public void recieveClientPacket(Player sender, PacketContainer packet) + throws IllegalAccessException, InvocationTargetException; + + /** + * Simulate recieving a certain packet from a given player. + * @param sender - the sender. + * @param packet - the packet that was sent. + * @param filters - whether or not to invoke any packet filters. + * @throws InvocationTargetException If the reflection machinery failed. + * @throws IllegalAccessException If the underlying method caused an error. + */ + public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) + throws IllegalAccessException, InvocationTargetException; +} diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java index 7a1ba112..1b8a7132 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolLibrary.java @@ -25,8 +25,10 @@ import org.bukkit.Server; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; +import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.metrics.Statistics; +import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; public class ProtocolLibrary extends JavaPlugin { @@ -39,10 +41,19 @@ public class ProtocolLibrary extends JavaPlugin { // Metrics and statistisc private Statistics statistisc; + // Structure compiler + private BackgroundCompiler backgroundCompiler; + + // Used to clean up server packets that have expired. + // But mostly required to simulate recieving client packets. + private int asyncPacketTask = -1; + private int tickCounter = 0; + private static final int ASYNC_PACKET_DELAY = 1; + @Override public void onLoad() { logger = getLoggerSafely(); - protocolManager = new PacketFilterManager(getClassLoader(), logger); + protocolManager = new PacketFilterManager(getClassLoader(), getServer(), logger); } @Override @@ -50,6 +61,12 @@ public class ProtocolLibrary extends JavaPlugin { Server server = getServer(); PluginManager manager = server.getPluginManager(); + // Initialize background compiler + if (backgroundCompiler == null) { + backgroundCompiler = new BackgroundCompiler(getClassLoader()); + BackgroundCompiler.setInstance(backgroundCompiler); + } + // Notify server managers of incompatible plugins checkForIncompatibility(manager); @@ -59,6 +76,9 @@ public class ProtocolLibrary extends JavaPlugin { // Inject our hook into already existing players protocolManager.initializePlayers(server.getOnlinePlayers()); + // Timeout + createAsyncTask(server); + // Try to enable statistics try { statistisc = new Statistics(this); @@ -69,9 +89,32 @@ public class ProtocolLibrary extends JavaPlugin { } } + private void createAsyncTask(Server server) { + try { + if (asyncPacketTask >= 0) + throw new IllegalStateException("Async task has already been created"); + + // Attempt to create task + asyncPacketTask = server.getScheduler().scheduleSyncRepeatingTask(this, new Runnable() { + @Override + public void run() { + AsyncFilterManager manager = (AsyncFilterManager) protocolManager.getAsynchronousManager(); + + // We KNOW we're on the main thread at the moment + manager.sendProcessedPackets(tickCounter++, true); + } + }, ASYNC_PACKET_DELAY, ASYNC_PACKET_DELAY); + + } catch (Throwable e) { + if (asyncPacketTask == -1) { + logger.log(Level.SEVERE, "Unable to create packet timeout task.", e); + } + } + } + private void checkForIncompatibility(PluginManager manager) { // Plugin authors: Notify me to remove these - String[] incompatiblePlugins = { "TagAPI" }; + String[] incompatiblePlugins = {}; for (String plugin : incompatiblePlugins) { if (manager.getPlugin(plugin) != null) { @@ -83,6 +126,19 @@ public class ProtocolLibrary extends JavaPlugin { @Override public void onDisable() { + // Disable compiler + if (backgroundCompiler != null) { + backgroundCompiler.shutdownAll(); + backgroundCompiler = null; + BackgroundCompiler.setInstance(null); + } + + // Clean up + if (asyncPacketTask >= 0) { + getServer().getScheduler().cancelTask(asyncPacketTask); + asyncPacketTask = -1; + } + protocolManager.close(); protocolManager = null; statistisc = null; diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java index 87062685..eaaf3702 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java @@ -17,7 +17,6 @@ package com.comphenix.protocol; -import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Set; @@ -35,7 +34,7 @@ import com.google.common.collect.ImmutableSet; * Represents an API for accessing the Minecraft protocol. * @author Kristian */ -public interface ProtocolManager { +public interface ProtocolManager extends PacketStream { /** * Retrieves a list of every registered packet listener. @@ -66,46 +65,6 @@ public interface ProtocolManager { * @param plugin - the plugin to unload. */ public void removePacketListeners(Plugin plugin); - - /** - * Send a packet to the given player. - * @param reciever - the reciever. - * @param packet - packet to send. - * @throws InvocationTargetException - if an error occured when sending the packet. - */ - public void sendServerPacket(Player reciever, PacketContainer packet) - throws InvocationTargetException; - - /** - * Send a packet to the given player. - * @param reciever - the reciever. - * @param packet - packet to send. - * @param filters - whether or not to invoke any packet filters. - * @throws InvocationTargetException - if an error occured when sending the packet. - */ - public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) - throws InvocationTargetException; - - /** - * Simulate recieving a certain packet from a given player. - * @param sender - the sender. - * @param packet - the packet that was sent. - * @throws InvocationTargetException If the reflection machinery failed. - * @throws IllegalAccessException If the underlying method caused an error. - */ - public void recieveClientPacket(Player sender, PacketContainer packet) - throws IllegalAccessException, InvocationTargetException; - - /** - * Simulate recieving a certain packet from a given player. - * @param sender - the sender. - * @param packet - the packet that was sent. - * @param filters - whether or not to invoke any packet filters. - * @throws InvocationTargetException If the reflection machinery failed. - * @throws IllegalAccessException If the underlying method caused an error. - */ - public void recieveClientPacket(Player sender, PacketContainer packet, boolean filters) - throws IllegalAccessException, InvocationTargetException; /** * Constructs a new encapsulated Minecraft packet with the given ID. @@ -163,4 +122,10 @@ public interface ProtocolManager { * @return TRUE if it has, FALSE otherwise. */ public boolean isClosed(); + + /** + * Retrieve the current asyncronous packet manager. + * @return Asyncronous packet manager. + */ + public AsynchronousManager getAsynchronousManager(); } \ No newline at end of file diff --git a/ProtocolLib/src/com/comphenix/protocol/async/AsyncFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/async/AsyncFilterManager.java new file mode 100644 index 00000000..0862bed4 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/AsyncFilterManager.java @@ -0,0 +1,301 @@ +package com.comphenix.protocol.async; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; + +import com.comphenix.protocol.AsynchronousManager; +import com.comphenix.protocol.PacketStream; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.PacketFilterManager; +import com.comphenix.protocol.injector.PrioritizedListener; +import com.google.common.base.Objects; + +/** + * Represents a filter manager for asynchronous packets. + * + * @author Kristian + */ +public class AsyncFilterManager implements AsynchronousManager { + + private PacketProcessingQueue serverProcessingQueue; + private PacketSendingQueue serverQueue; + + private PacketProcessingQueue clientProcessingQueue; + private PacketSendingQueue clientQueue; + + private Logger logger; + + // The likely main thread + private Thread mainThread; + + // Default scheduler + private BukkitScheduler scheduler; + + // Our protocol manager + private ProtocolManager manager; + + // Current packet index + private AtomicInteger currentSendingIndex = new AtomicInteger(); + + // Whether or not we're currently cleaning up + private volatile boolean cleaningUp; + + public AsyncFilterManager(Logger logger, BukkitScheduler scheduler, ProtocolManager manager) { + + // Server packets are synchronized already + this.serverQueue = new PacketSendingQueue(false); + // Client packets must be synchronized + this.clientQueue = new PacketSendingQueue(true); + + this.serverProcessingQueue = new PacketProcessingQueue(serverQueue); + this.clientProcessingQueue = new PacketProcessingQueue(clientQueue); + + this.scheduler = scheduler; + this.manager = manager; + + this.logger = logger; + this.mainThread = Thread.currentThread(); + } + + @Override + public AsyncListenerHandler registerAsyncHandler(PacketListener listener) { + AsyncListenerHandler handler = new AsyncListenerHandler(mainThread, this, listener); + + ListeningWhitelist sendingWhitelist = listener.getSendingWhitelist(); + ListeningWhitelist receivingWhitelist = listener.getReceivingWhitelist(); + + // We need a synchronized listener to get the ball rolling + boolean hasListener = true; + + // Add listener to either or both processing queue + if (hasValidWhitelist(sendingWhitelist)) { + PacketFilterManager.verifyWhitelist(listener, sendingWhitelist); + serverProcessingQueue.addListener(handler, sendingWhitelist); + hasListener &= hasPacketListener(sendingWhitelist); + } + + if (hasValidWhitelist(receivingWhitelist)) { + PacketFilterManager.verifyWhitelist(listener, receivingWhitelist); + clientProcessingQueue.addListener(handler, receivingWhitelist); + hasListener &= hasPacketListener(receivingWhitelist); + } + + if (!hasListener) { + handler.setNullPacketListener(new NullPacketListener(listener)); + manager.addPacketListener(handler.getNullPacketListener()); + } + + return handler; + } + + /** + * Determine if the given packets are represented. + * @param whitelist - list of packets. + * @return TRUE if they are all registered, FALSE otherwise. + */ + private boolean hasPacketListener(ListeningWhitelist whitelist) { + return manager.getSendingFilters().containsAll(whitelist.getWhitelist()); + } + + private boolean hasValidWhitelist(ListeningWhitelist whitelist) { + return whitelist != null && whitelist.getWhitelist().size() > 0; + } + + @Override + public void unregisterAsyncHandler(AsyncListenerHandler handler) { + if (handler == null) + throw new IllegalArgumentException("listenerToken cannot be NULL"); + + handler.cancel(); + } + + // Called by AsyncListenerHandler + void unregisterAsyncHandlerInternal(AsyncListenerHandler handler) { + + PacketListener listener = handler.getAsyncListener(); + boolean synchronusOK = onMainThread(); + + // Unregister null packet listeners + if (handler.getNullPacketListener() != null) { + manager.removePacketListener(handler.getNullPacketListener()); + } + + // Just remove it from the queue(s) + if (hasValidWhitelist(listener.getSendingWhitelist())) { + List removed = serverProcessingQueue.removeListener(handler, listener.getSendingWhitelist()); + + // We're already taking care of this, so don't do anything + if (!cleaningUp) + serverQueue.signalPacketUpdate(removed, synchronusOK); + } + + if (hasValidWhitelist(listener.getReceivingWhitelist())) { + List removed = clientProcessingQueue.removeListener(handler, listener.getReceivingWhitelist()); + + if (!cleaningUp) + clientQueue.signalPacketUpdate(removed, synchronusOK); + } + } + + /** + * Determine if we're running on the main thread. + * @return TRUE if we are, FALSE otherwise. + */ + private boolean onMainThread() { + return Thread.currentThread().getId() == mainThread.getId(); + } + + @Override + public void unregisterAsyncHandlers(Plugin plugin) { + unregisterAsyncHandlers(serverProcessingQueue, plugin); + unregisterAsyncHandlers(clientProcessingQueue, plugin); + } + + private void unregisterAsyncHandlers(PacketProcessingQueue processingQueue, Plugin plugin) { + + // Iterate through every packet listener + for (PrioritizedListener listener : processingQueue.values()) { + // Remove the listener + if (Objects.equal(listener.getListener().getPlugin(), plugin)) { + unregisterAsyncHandler(listener.getListener()); + } + } + } + + /** + * Enqueue a packet for asynchronous processing. + * @param syncPacket - synchronous packet event. + * @param asyncMarker - the asynchronous marker to use. + */ + public void enqueueSyncPacket(PacketEvent syncPacket, AsyncMarker asyncMarker) { + PacketEvent newEvent = PacketEvent.fromSynchronous(syncPacket, asyncMarker); + + // Start the process + getSendingQueue(syncPacket).enqueue(newEvent); + + // We know this is occuring on the main thread, so pass TRUE + getProcessingQueue(syncPacket).enqueue(newEvent, true); + } + + @Override + public Set getSendingFilters() { + return serverProcessingQueue.keySet(); + } + + @Override + public Set getReceivingFilters() { + return clientProcessingQueue.keySet(); + } + + /** + * Used to create a default asynchronous task. + * @param plugin - the calling plugin. + * @param runnable - the runnable. + */ + public void scheduleAsyncTask(Plugin plugin, Runnable runnable) { + scheduler.scheduleAsyncDelayedTask(plugin, runnable); + } + + @Override + public boolean hasAsynchronousListeners(PacketEvent packet) { + Collection list = getProcessingQueue(packet).getListener(packet.getPacketID()); + return list != null && list.size() > 0; + } + + /** + * Construct a asynchronous marker with all the default values. + * @return Asynchronous marker. + */ + public AsyncMarker createAsyncMarker() { + return createAsyncMarker(AsyncMarker.DEFAULT_SENDING_DELTA, AsyncMarker.DEFAULT_TIMEOUT_DELTA); + } + + /** + * Construct an async marker with the given sending priority delta and timeout delta. + * @param sendingDelta - how many packets we're willing to wait. + * @param timeoutDelta - how long (in ms) until the packet expire. + * @return An async marker. + */ + public AsyncMarker createAsyncMarker(long sendingDelta, long timeoutDelta) { + return createAsyncMarker(sendingDelta, timeoutDelta, + currentSendingIndex.incrementAndGet(), System.currentTimeMillis()); + } + + // Helper method + private AsyncMarker createAsyncMarker(long sendingDelta, long timeoutDelta, long sendingIndex, long currentTime) { + return new AsyncMarker(manager, sendingIndex, sendingDelta, System.currentTimeMillis(), timeoutDelta); + } + + @Override + public PacketStream getPacketStream() { + return manager; + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public void cleanupAll() { + cleaningUp = true; + serverProcessingQueue.cleanupAll(); + serverQueue.cleanupAll(); + } + + /** + * Signal that a packet is ready to be transmitted. + * @param packet - packet to signal. + */ + public void signalPacketUpdate(PacketEvent packet) { + getSendingQueue(packet).signalPacketUpdate(packet, onMainThread()); + } + + /** + * Retrieve the sending queue this packet belongs to. + * @param packet - the packet. + * @return The server or client sending queue the packet belongs to. + */ + private PacketSendingQueue getSendingQueue(PacketEvent packet) { + return packet.isServerPacket() ? serverQueue : clientQueue; + } + + /** + * Signal that a packet has finished processing. + * @param packet - packet to signal. + */ + public void signalProcessingDone(PacketEvent packet) { + getProcessingQueue(packet).signalProcessingDone(); + } + + /** + * Retrieve the processing queue this packet belongs to. + * @param packet - the packet. + * @return The server or client sending processing the packet belongs to. + */ + private PacketProcessingQueue getProcessingQueue(PacketEvent packet) { + return packet.isServerPacket() ? serverProcessingQueue : clientProcessingQueue; + } + + /** + * Send any due packets, or clean up packets that have expired. + */ + public void sendProcessedPackets(int tickCounter, boolean onMainThread) { + + // The server queue is unlikely to need checking that often + if (tickCounter % 10 == 0) { + serverQueue.trySendPackets(onMainThread); + } + + clientQueue.trySendPackets(onMainThread); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/async/AsyncListenerHandler.java b/ProtocolLib/src/com/comphenix/protocol/async/AsyncListenerHandler.java new file mode 100644 index 00000000..e4cd15fc --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/AsyncListenerHandler.java @@ -0,0 +1,210 @@ +package com.comphenix.protocol.async; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.logging.Level; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; + +/** + * Represents a handler for an asynchronous event. + * + * @author Kristian + */ +public class AsyncListenerHandler { + + /** + * Signal an end to the packet processing. + */ + private static final PacketEvent INTERUPT_PACKET = new PacketEvent(new Object()); + + // Default queue capacity + private static int DEFAULT_CAPACITY = 1024; + + // Cancel the async handler + private volatile boolean cancelled; + + // If we've started the listener loop before + private volatile boolean started; + + // The packet listener + private PacketListener listener; + + // The filter manager + private AsyncFilterManager filterManager; + private NullPacketListener nullPacketListener; + + // List of queued packets + private ArrayBlockingQueue queuedPackets = new ArrayBlockingQueue(DEFAULT_CAPACITY); + + // Minecraft main thread + private Thread mainThread; + + public AsyncListenerHandler(Thread mainThread, AsyncFilterManager filterManager, PacketListener listener) { + if (filterManager == null) + throw new IllegalArgumentException("filterManager cannot be NULL"); + if (listener == null) + throw new IllegalArgumentException("listener cannot be NULL"); + + this.mainThread = mainThread; + this.filterManager = filterManager; + this.listener = listener; + } + + public boolean isCancelled() { + return cancelled; + } + + public PacketListener getAsyncListener() { + return listener; + } + + /** + * Set the synchronized listener that has been automatically created. + * @param nullPacketListener - automatically created listener. + */ + void setNullPacketListener(NullPacketListener nullPacketListener) { + this.nullPacketListener = nullPacketListener; + } + + /** + * Retrieve the synchronized listener that was automatically created. + * @return Automatically created listener. + */ + PacketListener getNullPacketListener() { + return nullPacketListener; + } + + /** + * Cancel the handler. + */ + public void cancel() { + // Remove the listener as quickly as possible + close(); + + // Poison Pill Shutdown + queuedPackets.clear(); + queuedPackets.add(INTERUPT_PACKET); + } + + /** + * Queue a packet for processing. + * @param packet - a packet for processing. + * @throws IllegalStateException If the underlying packet queue is full. + */ + public void enqueuePacket(PacketEvent packet) { + if (packet == null) + throw new IllegalArgumentException("packet is NULL"); + + queuedPackets.add(packet); + } + + /** + * Create a runnable that will initiate the listener loop. + *

+ * Warning: Never call the run() method in the main thread. + */ + public Runnable getListenerLoop() { + return new Runnable() { + @Override + public void run() { + listenerLoop(); + } + }; + } + + // DO NOT call this method from the main thread + private void listenerLoop() { + + // Danger, danger! + if (Thread.currentThread().getId() == mainThread.getId()) + throw new IllegalStateException("Do not call this method from the main thread."); + if (started) + throw new IllegalStateException("A listener cannot be run by multiple threads. Create a new listener instead."); + if (cancelled) + throw new IllegalStateException("Listener has been cancelled. Create a new listener instead."); + + // Proceed + started = true; + + try { + mainLoop: + while (!cancelled) { + PacketEvent packet = queuedPackets.take(); + AsyncMarker marker = packet.getAsyncMarker(); + + // Handle cancel requests + if (packet == null || marker == null || !packet.isAsynchronous()) { + break; + } + + // Here's the core of the asynchronous processing + try { + if (packet.isServerPacket()) + listener.onPacketSending(packet); + else + listener.onPacketReceiving(packet); + + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + filterManager.getLogger().log(Level.SEVERE, + "Unhandled exception occured in onAsyncPacket() for " + getPluginName(), e); + } + + // Now, get the next non-cancelled listener + for (; marker.getListenerTraversal().hasNext(); ) { + AsyncListenerHandler handler = marker.getListenerTraversal().next().getListener(); + + if (!handler.isCancelled()) { + handler.enqueuePacket(packet); + continue mainLoop; + } + } + + // There are no more listeners - queue the packet for transmission + filterManager.signalPacketUpdate(packet); + filterManager.signalProcessingDone(packet); + } + + } catch (InterruptedException e) { + // We're done + } + + // Clean up + close(); + } + + private void close() { + // Remove the listener itself + if (!cancelled) { + filterManager.unregisterAsyncHandlerInternal(this); + cancelled = true; + started = false; + } + } + + private String getPluginName() { + return PacketAdapter.getPluginName(listener); + } + + /** + * Retrieve the plugin associated with this async listener. + * @return The plugin. + */ + public Plugin getPlugin() { + return listener != null ? listener.getPlugin() : null; + } + + /** + * Start the asynchronous listener using the Bukkit scheduler. + */ + public void start() { + if (listener.getPlugin() == null) + throw new IllegalArgumentException("Cannot start task without a valid plugin."); + + filterManager.scheduleAsyncTask(listener.getPlugin(), getListenerLoop()); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/async/AsyncMarker.java b/ProtocolLib/src/com/comphenix/protocol/async/AsyncMarker.java new file mode 100644 index 00000000..8cecb1c8 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/AsyncMarker.java @@ -0,0 +1,305 @@ +package com.comphenix.protocol.async; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; + +import net.minecraft.server.Packet; + +import com.comphenix.protocol.PacketStream; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.PrioritizedListener; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.primitives.Longs; + +/** + * Contains information about the packet that is being processed by asynchronous listeners. + *

+ * Asynchronous listeners can use this to set packet timeout or transmission order. + * + * @author Kristian + */ +public class AsyncMarker implements Serializable, Comparable { + + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = -2621498096616187384L; + + /** + * Default number of milliseconds until a packet will rejected. + */ + public static final int DEFAULT_TIMEOUT_DELTA = 60000; + + /** + * Default number of packets to skip. + */ + public static final int DEFAULT_SENDING_DELTA = 0; + + /** + * The packet stream responsible for transmitting the packet when it's done processing. + */ + private transient PacketStream packetStream; + + /** + * Current list of async packet listeners. + */ + private transient Iterator> listenerTraversal; + + // Timeout handling + private long initialTime; + private long timeout; + + // Packet order + private long originalSendingIndex; + private long newSendingIndex; + + // Whether or not the packet has been processed by the listeners + private volatile boolean processed; + + // Whether or not the packet has been sent + private volatile boolean transmitted; + + // Whether or not the asynchronous processing itself should be cancelled + private volatile boolean asyncCancelled; + + // Determine if Minecraft processes this packet asynchronously + private static Method isMinecraftAsync; + private static boolean alwaysSync; + + /** + * Create a container for asyncronous packets. + * @param initialTime - the current time in milliseconds since 01.01.1970 00:00. + */ + AsyncMarker(PacketStream packetStream, long sendingIndex, long sendingDelta, long initialTime, long timeoutDelta) { + if (packetStream == null) + throw new IllegalArgumentException("packetStream cannot be NULL"); + + this.packetStream = packetStream; + + // Timeout + this.initialTime = initialTime; + this.timeout = initialTime + timeoutDelta; + + // Sending index + this.originalSendingIndex = sendingIndex; + this.newSendingIndex = sendingIndex; + } + + /** + * Retrieve the time the packet was initially queued for asynchronous processing. + * @return The initial time in number of milliseconds since 01.01.1970 00:00. + */ + public long getInitialTime() { + return initialTime; + } + + /** + * Retrieve the time the packet will be forcefully rejected. + * @return The time to reject the packet, in milliseconds since 01.01.1970 00:00. + */ + public long getTimeout() { + return timeout; + } + + /** + * Set the time the packet will be forcefully rejected. + * @param timeout - time to reject the packet, in milliseconds since 01.01.1970 00:00. + */ + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + /** + * Retrieve the order the packet was originally transmitted. + * @return The original packet index. + */ + public long getOriginalSendingIndex() { + return originalSendingIndex; + } + + /** + * Retrieve the desired sending order after processing has completed. + *

+ * Higher sending order means lower priority. + * @return Desired sending order. + */ + public long getNewSendingIndex() { + return newSendingIndex; + } + + /** + * Sets the desired sending order after processing has completed. + *

+ * Higher sending order means lower priority. + * @param newSendingIndex - new packet send index. + */ + public void setNewSendingIndex(long newSendingIndex) { + this.newSendingIndex = newSendingIndex; + } + + /** + * Retrieve the packet stream responsible for transmitting this packet. + * @return The packet stream. + */ + public PacketStream getPacketStream() { + return packetStream; + } + + /** + * Sets the output packet stream responsible for transmitting this packet. + * @param packetStream - new output packet stream. + */ + public void setPacketStream(PacketStream packetStream) { + this.packetStream = packetStream; + } + + /** + * Retrieve whether or not this packet has been processed by the async listeners. + * @return TRUE if it has been processed, FALSE otherwise. + */ + public boolean isProcessed() { + return processed; + } + + /** + * Sets whether or not this packet has been processed by the async listeners. + * @param processed - TRUE if it has, FALSE otherwise. + */ + void setProcessed(boolean processed) { + this.processed = processed; + } + + /** + * Retrieve whether or not this packet has already been sent. + * @return TRUE if it has been sent before, FALSE otherwise. + */ + public boolean isTransmitted() { + return transmitted; + } + + /** + * Determine if this packet has expired. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean hasExpired() { + return hasExpired(System.currentTimeMillis()); + } + + /** + * Determine if this packet has expired given this time. + * @param currentTime - the current time in milliseconds since 01.01.1970 00:00. + * @return TRUE if it has, FALSE otherwise. + */ + public boolean hasExpired(long currentTime) { + return timeout < currentTime; + } + + /** + * Determine if the asynchronous handling should be cancelled. + * @return TRUE if it should, FALSE otherwise. + */ + public boolean isAsyncCancelled() { + return asyncCancelled; + } + + /** + * Set whether or not the asynchronous handling should be cancelled. + * @param asyncCancelled - TRUE to cancel it, FALSE otherwise. + */ + public void setAsyncCancelled(boolean asyncCancelled) { + this.asyncCancelled = asyncCancelled; + } + + /** + * Retrieve iterator for the next listener in line. + * @return Next async packet listener iterator. + */ + public Iterator> getListenerTraversal() { + return listenerTraversal; + } + + /** + * Set the iterator for the next listener. + * @param listenerTraversal - the new async packet listener iterator. + */ + void setListenerTraversal(Iterator> listenerTraversal) { + this.listenerTraversal = listenerTraversal; + } + + /** + * Transmit a given packet to the current packet stream. + * @param event - the packet to send. + * @throws IOException If the packet couldn't be sent. + */ + public void sendPacket(PacketEvent event) throws IOException { + try { + if (event.isServerPacket()) { + packetStream.sendServerPacket(event.getPlayer(), event.getPacket(), false); + } else { + packetStream.recieveClientPacket(event.getPlayer(), event.getPacket(), false); + } + transmitted = true; + + } catch (InvocationTargetException e) { + throw new IOException("Cannot send packet", e); + } catch (IllegalAccessException e) { + throw new IOException("Cannot send packet", e); + } + } + + /** + * Determine if Minecraft allows asynchronous processing of this packet. + * @return TRUE if it does, FALSE otherwise. + */ + public boolean isMinecraftAsync(PacketEvent event) throws FieldAccessException { + + if (isMinecraftAsync == null && !alwaysSync) { + try { + isMinecraftAsync = FuzzyReflection.fromClass(Packet.class).getMethodByName("a_.*"); + } catch (RuntimeException e) { + // This will occur in 1.2.5 (or possibly in later versions) + List methods = FuzzyReflection.fromClass(Packet.class). + getMethodListByParameters(boolean.class, new Class[] {}); + + // Try to look for boolean methods + if (methods.size() == 2) { + isMinecraftAsync = methods.get(1); + } else if (methods.size() == 1) { + // We're in 1.2.5 + alwaysSync = true; + } else { + System.err.println("Cannot determine asynchronous state of packets!"); + alwaysSync = true; + } + } + } + + if (alwaysSync) { + return false; + } else { + try { + // Wrap exceptions + return (Boolean) isMinecraftAsync.invoke(event.getPacket().getHandle()); + } catch (IllegalArgumentException e) { + throw new FieldAccessException("Illegal argument", e); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Unable to reflect method call 'a_', or: isAsyncPacket.", e); + } catch (InvocationTargetException e) { + throw new FieldAccessException("Minecraft error", e); + } + } + } + + @Override + public int compareTo(AsyncMarker o) { + if (o == null) + return 1; + else + return Longs.compare(getNewSendingIndex(), o.getNewSendingIndex()); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/async/NullPacketListener.java b/ProtocolLib/src/com/comphenix/protocol/async/NullPacketListener.java new file mode 100644 index 00000000..d02559d3 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/NullPacketListener.java @@ -0,0 +1,62 @@ +package com.comphenix.protocol.async; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; + +/** + * Represents a NO OPERATION listener. + * + * @author Kristian + */ +class NullPacketListener implements PacketListener { + + private ListeningWhitelist sendingWhitelist; + private ListeningWhitelist receivingWhitelist; + private Plugin plugin; + + /** + * Create a no-op listener with the same whitelist and plugin as the given listener. + * @param original - the packet listener to copy. + */ + public NullPacketListener(PacketListener original) { + this.sendingWhitelist = cloneWhitelist(ListenerPriority.LOW, original.getSendingWhitelist()); + this.receivingWhitelist = cloneWhitelist(ListenerPriority.LOW, original.getReceivingWhitelist()); + this.plugin = original.getPlugin(); + } + + @Override + public void onPacketSending(PacketEvent event) { + // NULL + } + + @Override + public void onPacketReceiving(PacketEvent event) { + // NULL + } + + @Override + public ListeningWhitelist getSendingWhitelist() { + return sendingWhitelist; + } + + @Override + public ListeningWhitelist getReceivingWhitelist() { + return receivingWhitelist; + } + + private ListeningWhitelist cloneWhitelist(ListenerPriority priority, ListeningWhitelist whitelist) { + if (whitelist != null) + return new ListeningWhitelist(priority, whitelist.getWhitelist()); + else + return null; + } + + @Override + public Plugin getPlugin() { + return plugin; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/async/PacketProcessingQueue.java b/ProtocolLib/src/com/comphenix/protocol/async/PacketProcessingQueue.java new file mode 100644 index 00000000..37349e83 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/PacketProcessingQueue.java @@ -0,0 +1,133 @@ +package com.comphenix.protocol.async; + +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Semaphore; + +import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.PrioritizedListener; + +/** + * Handles the processing of every packet type. + * + * @author Kristian + */ +class PacketProcessingQueue extends AbstractConcurrentListenerMultimap { + + /** + * Default maximum number of packets to process concurrently. + */ + public static final int DEFAULT_MAXIMUM_CONCURRENCY = 32; + + /** + * Default maximum number of packets to queue for processing. + */ + public static final int DEFAULT_QUEUE_LIMIT = 1024 * 60; + + /** + * Number of packets we're processing concurrently. + */ + private final int maximumConcurrency; + private Semaphore concurrentProcessing; + + // Queued packets for being processed + private ArrayBlockingQueue processingQueue; + + // Packets for sending + private PacketSendingQueue sendingQueue; + + public PacketProcessingQueue(PacketSendingQueue sendingQueue) { + this(sendingQueue, DEFAULT_QUEUE_LIMIT, DEFAULT_MAXIMUM_CONCURRENCY); + } + + public PacketProcessingQueue(PacketSendingQueue sendingQueue, int queueLimit, int maximumConcurrency) { + super(); + this.processingQueue = new ArrayBlockingQueue(queueLimit); + this.maximumConcurrency = maximumConcurrency; + this.concurrentProcessing = new Semaphore(maximumConcurrency); + this.sendingQueue = sendingQueue; + } + + /** + * Enqueue a packet for processing by the asynchronous listeners. + * @param packet - packet to process. + * @param onMainThread - whether or not this is occuring on the main thread. + * @return TRUE if we sucessfully queued the packet, FALSE if the queue ran out if space. + */ + public boolean enqueue(PacketEvent packet, boolean onMainThread) { + try { + processingQueue.add(packet); + + // Begin processing packets + signalBeginProcessing(onMainThread); + return true; + } catch (IllegalStateException e) { + return false; + } + } + + /** + * Called by the current method and each thread to signal that a packet might be ready for processing. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public void signalBeginProcessing(boolean onMainThread) { + while (concurrentProcessing.tryAcquire()) { + PacketEvent packet = processingQueue.poll(); + + // Any packet queued? + if (packet != null) { + Collection> list = getListener(packet.getPacketID()); + AsyncMarker marker = packet.getAsyncMarker(); + + // Yes, removing the marker will cause the chain to stop + if (list != null) { + Iterator> iterator = list.iterator(); + + if (iterator.hasNext()) { + marker.setListenerTraversal(iterator); + iterator.next().getListener().enqueuePacket(packet); + continue; + } + } + + // The packet has no further listeners. Just send it. + sendingQueue.signalPacketUpdate(packet, onMainThread); + signalProcessingDone(); + + } else { + // No more queued packets. + signalProcessingDone(); + return; + } + } + } + + /** + * Called when a packet has been processed. + */ + public void signalProcessingDone() { + concurrentProcessing.release(); + } + + /** + * Retrieve the maximum number of packets to process at any given time. + * @return Number of simultaneous packet to process. + */ + public int getMaximumConcurrency() { + return maximumConcurrency; + } + + public void cleanupAll() { + // Cancel all the threads and every listener + for (PrioritizedListener handler : values()) { + if (handler != null) { + handler.getListener().cancel(); + } + } + + // Remove the rest, just in case + clearListeners(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/async/PacketSendingQueue.java b/ProtocolLib/src/com/comphenix/protocol/async/PacketSendingQueue.java new file mode 100644 index 00000000..500cf2c3 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/async/PacketSendingQueue.java @@ -0,0 +1,177 @@ +package com.comphenix.protocol.async; + +import java.io.IOException; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.PriorityBlockingQueue; + +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.google.common.collect.ComparisonChain; + +/** + * Represents packets ready to be transmitted to a client. + * @author Kristian + */ +class PacketSendingQueue { + + private static final int INITIAL_CAPACITY = 64; + + private PriorityBlockingQueue sendingQueue; + + // Whether or not packet transmission can only occur on the main thread + private final boolean synchronizeMain; + + /** + * Create a packet sending queue. + * @param synchronizeMain - whether or not to synchronize with the main thread. + */ + public PacketSendingQueue(boolean synchronizeMain) { + this.synchronizeMain = synchronizeMain; + this.sendingQueue = new PriorityBlockingQueue(INITIAL_CAPACITY, new Comparator() { + // Compare using the async marker + @Override + public int compare(PacketEvent o1, PacketEvent o2) { + return ComparisonChain.start(). + compare(o1.getAsyncMarker(), o2.getAsyncMarker()). + result(); + } + }); + } + + /** + * Enqueue a packet for sending. + * @param packet + */ + public void enqueue(PacketEvent packet) { + sendingQueue.add(packet); + } + + /** + * Invoked when one of the packets have finished processing. + * @param packetUpdated - the packet that has now been updated. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(PacketEvent packetUpdated, boolean onMainThread) { + // Mark this packet as finished + packetUpdated.getAsyncMarker().setProcessed(true); + trySendPackets(onMainThread); + } + + /*** + * Invoked when a list of packet IDs are no longer associated with any listeners. + * @param packetsRemoved - packets that no longer have any listeners. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public synchronized void signalPacketUpdate(List packetsRemoved, boolean onMainThread) { + + Set lookup = new HashSet(packetsRemoved); + + // Note that this is O(n), so it might be expensive + for (PacketEvent event : sendingQueue) { + if (lookup.contains(event.getPacketID())) { + event.getAsyncMarker().setProcessed(true); + } + } + + // This is likely to have changed the situation a bit + trySendPackets(onMainThread); + } + + /** + * Attempt to send any remaining packets. + * @param onMainThread - whether or not this is occuring on the main thread. + */ + public void trySendPackets(boolean onMainThread) { + + // Transmit as many packets as we can + while (true) { + PacketEvent current = sendingQueue.peek(); + + if (current != null) { + AsyncMarker marker = current.getAsyncMarker(); + + // Abort if we're not on the main thread + if (synchronizeMain) { + try { + boolean wantAsync = marker.isMinecraftAsync(current); + boolean wantSync = !wantAsync; + + // Quit if we haven't fulfilled our promise + if ((onMainThread && wantAsync) || (!onMainThread && wantSync)) + return; + + } catch (FieldAccessException e) { + e.printStackTrace(); + return; + } + } + + if (marker.isProcessed() || marker.hasExpired()) { + if (marker.isProcessed() && !current.isCancelled()) { + sendPacket(current); + } + + sendingQueue.poll(); + continue; + } + } + + // Only repeat when packets are removed + break; + } + } + + /** + * Send every packet, regardless of the processing state. + */ + private void forceSend() { + while (true) { + PacketEvent current = sendingQueue.poll(); + + if (current != null) { + sendPacket(current); + } else { + break; + } + } + } + + /** + * Whether or not the packet transmission must synchronize with the main thread. + * @return TRUE if it must, FALSE otherwise. + */ + public boolean isSynchronizeMain() { + return synchronizeMain; + } + + /** + * Transmit a packet, if it hasn't already. + * @param event - the packet to transmit. + */ + private void sendPacket(PacketEvent event) { + + AsyncMarker marker = event.getAsyncMarker(); + + try { + // Don't send a packet twice + if (marker != null && !marker.isTransmitted()) { + marker.sendPacket(event); + } + + } catch (IOException e) { + // Just print the error + e.printStackTrace(); + } + } + + /** + * Automatically transmits every delayed packet. + */ + public void cleanupAll() { + // Note that the cleanup itself will always occur on the main thread + forceSend(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java b/ProtocolLib/src/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java new file mode 100644 index 00000000..00741fe2 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/concurrency/AbstractConcurrentListenerMultimap.java @@ -0,0 +1,131 @@ +package com.comphenix.protocol.concurrency; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.injector.PrioritizedListener; +import com.google.common.collect.Iterables; + +/** + * A thread-safe implementation of a listener multimap. + * + * @author Kristian + */ +public abstract class AbstractConcurrentListenerMultimap { + + // The core of our map + private ConcurrentMap>> listeners = + new ConcurrentHashMap>>(); + + /** + * Adds a listener to its requested list of packet recievers. + * @param listener - listener with a list of packets to recieve notifcations for. + * @param whitelist - the packet whitelist to use. + */ + public void addListener(TListener listener, ListeningWhitelist whitelist) { + + PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority()); + + for (Integer packetID : whitelist.getWhitelist()) { + addListener(packetID, prioritized); + } + } + + // Add the listener to a specific packet notifcation list + private void addListener(Integer packetID, PrioritizedListener listener) { + + SortedCopyOnWriteArray> list = listeners.get(packetID); + + // We don't want to create this for every lookup + if (list == null) { + // It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order, + // which is a essential feature for our purposes. + final SortedCopyOnWriteArray> value = new SortedCopyOnWriteArray>(); + + list = listeners.putIfAbsent(packetID, value); + + // We may end up creating multiple multisets, but we'll agree + // on the one to use. + if (list == null) { + list = value; + } + } + + // Thread safe + list.add(listener); + } + + /** + * Removes the given listener from the packet event list. + * @param listener - listener to remove. + * @param whitelist - the packet whitelist that was used. + * @return Every packet ID that was removed due to no listeners. + */ + public List removeListener(TListener listener, ListeningWhitelist whitelist) { + + List removedPackets = new ArrayList(); + + // Again, not terribly efficient. But adding or removing listeners should be a rare event. + for (Integer packetID : whitelist.getWhitelist()) { + + SortedCopyOnWriteArray> list = listeners.get(packetID); + + // Remove any listeners + if (list != null) { + // Don't remove from newly created lists + if (list.size() > 0) { + // Remove this listener. Note that priority is generally ignored. + list.remove(new PrioritizedListener(listener, whitelist.getPriority())); + + if (list.size() == 0) { + listeners.remove(packetID); + removedPackets.add(packetID); + } + } + } + + // Move on to the next + } + + return removedPackets; + } + + /** + * Retrieve the registered listeners, in order from the lowest to the highest priority. + *

+ * The returned list is thread-safe and doesn't require synchronization. + * @param packetID - packet ID. + * @return Registered listeners. + */ + public Collection> getListener(int packetID) { + return listeners.get(packetID); + } + + /** + * Retrieve every listener. + * @return Every listener. + */ + public Iterable> values() { + return Iterables.concat(listeners.values()); + } + + /** + * Retrieve every registered packet ID: + * @return Registered packet ID. + */ + public Set keySet() { + return listeners.keySet(); + } + + /** + * Remove all packet listeners. + */ + protected void clearListeners() { + listeners.clear(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java b/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java index ee1a3870..cca5740d 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/ListeningWhitelist.java @@ -62,7 +62,7 @@ public class ListeningWhitelist { * @return TRUE if there are any packets, FALSE otherwise. */ public boolean isEnabled() { - return whitelist != null || whitelist.size() > 0; + return whitelist != null && whitelist.size() > 0; } /** @@ -86,6 +86,23 @@ public class ListeningWhitelist { return Objects.hashCode(priority, whitelist); } + /** + * Determine if any of the given IDs can be found in the whitelist. + * @param whitelist - whitelist to test. + * @param idList - list of packet IDs to find. + * @return TRUE if any of the packets in the list can be found in the whitelist, FALSE otherwise. + */ + public static boolean containsAny(ListeningWhitelist whitelist, int... idList) { + if (whitelist != null) { + for (int i = 0; i < idList.length; i++) { + if (whitelist.getWhitelist().contains(idList[i])) + return true; + } + } + + return false; + } + @Override public boolean equals(final Object obj){ if(obj instanceof ListeningWhitelist){ diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java index 09207da9..34bd0a49 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketContainer.java @@ -17,6 +17,12 @@ package com.comphenix.protocol.events; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -40,13 +46,18 @@ import net.minecraft.server.Packet; * * @author Kristian */ -public class PacketContainer { +public class PacketContainer implements Serializable { - protected Packet handle; - protected int id; + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = 2074805748222377230L; + protected int id; + protected transient Packet handle; + // Current structure modifier - protected StructureModifier structureModifier; + protected transient StructureModifier structureModifier; // Check whether or not certain classes exists private static boolean hasWorldType = false; @@ -54,6 +65,10 @@ public class PacketContainer { // The getEntity method private static Method getEntity; + // Support for serialization + private static Method writeMethod; + private static Method readMethod; + static { try { Class.forName("net.minecraft.server.WorldType"); @@ -291,4 +306,60 @@ public class PacketContainer { public int getID() { return id; } + + private void writeObject(ObjectOutputStream output) throws IOException { + // Default serialization + output.defaultWriteObject(); + + // We'll take care of NULL packets as well + output.writeBoolean(handle != null); + + // Retrieve the write method by reflection + if (writeMethod == null) + writeMethod = FuzzyReflection.fromObject(handle).getMethodByParameters("write", DataOutputStream.class); + + try { + // Call the write-method + writeMethod.invoke(handle, new DataOutputStream(output)); + } catch (IllegalArgumentException e) { + throw new IOException("Minecraft packet doesn't support DataOutputStream", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Insufficient security privileges.", e); + } catch (InvocationTargetException e) { + throw new IOException("Could not serialize Minecraft packet.", e); + } + } + + private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { + // Default deserialization + input.defaultReadObject(); + + // Get structure modifier + structureModifier = StructureCache.getStructure(id); + + // Don't read NULL packets + if (input.readBoolean()) { + + // Create a default instance of the packet + handle = StructureCache.newPacket(id); + + // Retrieve the read method by reflection + if (readMethod == null) + readMethod = FuzzyReflection.fromObject(handle).getMethodByParameters("read", DataInputStream.class); + + // Call the read method + try { + readMethod.invoke(handle, new DataInputStream(input)); + } catch (IllegalArgumentException e) { + throw new IOException("Minecraft packet doesn't support DataInputStream", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Insufficient security privileges.", e); + } catch (InvocationTargetException e) { + throw new IOException("Could not deserialize Minecraft packet.", e); + } + + // And we're done + structureModifier = structureModifier.withTarget(handle); + } + } } diff --git a/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java b/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java index e4734e17..92b039fa 100644 --- a/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java +++ b/ProtocolLib/src/com/comphenix/protocol/events/PacketEvent.java @@ -17,22 +17,30 @@ package com.comphenix.protocol.events; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.util.EventObject; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; +import com.comphenix.protocol.async.AsyncMarker; + public class PacketEvent extends EventObject implements Cancellable { /** * Automatically generated by Eclipse. */ private static final long serialVersionUID = -5360289379097430620L; + private transient Player player; private PacketContainer packet; - private Player player; private boolean serverPacket; private boolean cancel; + private AsyncMarker asyncMarker; + private boolean asynchronous; + /** * Use the static constructors to create instances of this event. * @param source - the event source. @@ -47,6 +55,16 @@ public class PacketEvent extends EventObject implements Cancellable { this.player = player; this.serverPacket = serverPacket; } + + private PacketEvent(PacketEvent origial, AsyncMarker asyncMarker) { + super(origial.source); + this.packet = origial.packet; + this.player = origial.player; + this.cancel = origial.cancel; + this.serverPacket = origial.serverPacket; + this.asyncMarker = asyncMarker; + this.asynchronous = true; + } /** * Creates an event representing a client packet transmission. @@ -70,6 +88,16 @@ public class PacketEvent extends EventObject implements Cancellable { return new PacketEvent(source, packet, recipient, true); } + /** + * Create an asynchronous packet event from a synchronous event and a async marker. + * @param event - the original synchronous event. + * @param marker - the asynchronous marker. + * @return The new packet event. + */ + public static PacketEvent fromSynchronous(PacketEvent event, AsyncMarker marker) { + return new PacketEvent(event, marker); + } + /** * Retrieves the packet that will be sent to the player. * @return Packet to send to the player. @@ -120,9 +148,67 @@ public class PacketEvent extends EventObject implements Cancellable { /** * Whether or not this packet was created by the server. + *

+ * Most listeners can deduce this by noting which listener method was invoked. * @return TRUE if the packet was created by the server, FALSE if it was created by a client. */ public boolean isServerPacket() { return serverPacket; } + + /** + * Retrieve the asynchronous marker. + *

+ * If the packet is synchronous, this marker will be used to schedule an asynchronous event. In the following + * asynchronous event, the marker is used to correctly pass the packet around to the different threads. + *

+ * Note that if there are no asynchronous events that can receive this packet, the marker is NULL. + * @return The current asynchronous marker, or NULL. + */ + public AsyncMarker getAsyncMarker() { + return asyncMarker; + } + /** + * Set the asynchronous marker. + *

+ * If the marker is non-null at the end of an synchronous event processing, the packet will be scheduled + * to be processed asynchronously with the given settings. + *

+ * Note that if there are no asynchronous events that can receive this packet, the marker should be NULL. + * @param asyncMarker - the new asynchronous marker, or NULL. + * @throws IllegalStateException If the current event is asynchronous. + */ + public void setAsyncMarker(AsyncMarker asyncMarker) { + if (isAsynchronous()) + throw new IllegalStateException("The marker is immutable for asynchronous events"); + this.asyncMarker = asyncMarker; + } + + /** + * Determine if the packet event has been executed asynchronously or not. + * @return TRUE if this packet event is asynchronous, FALSE otherwise. + */ + public boolean isAsynchronous() { + return asynchronous; + } + + private void writeObject(ObjectOutputStream output) throws IOException { + // Default serialization + output.defaultWriteObject(); + + // Write the name of the player (or NULL if it's not set) + output.writeObject(player != null ? new SerializedOfflinePlayer(player) : null); + } + + private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { + // Default deserialization + input.defaultReadObject(); + + final SerializedOfflinePlayer offlinePlayer = (SerializedOfflinePlayer) input.readObject(); + + if (offlinePlayer != null) { + // Better than nothing + player = offlinePlayer.getPlayer(); + } + } } diff --git a/ProtocolLib/src/com/comphenix/protocol/events/SerializedOfflinePlayer.java b/ProtocolLib/src/com/comphenix/protocol/events/SerializedOfflinePlayer.java new file mode 100644 index 00000000..4e222e57 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/events/SerializedOfflinePlayer.java @@ -0,0 +1,216 @@ +package com.comphenix.protocol.events; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.entity.Player; + +/** + * Represents a player object that can be serialized by Java. + * + * @author Kristian + */ +class SerializedOfflinePlayer implements OfflinePlayer, Serializable { + + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = -2728976288470282810L; + + private transient Location bedSpawnLocation; + + // Relevant data about an offline player + private String name; + private long firstPlayed; + private long lastPlayed; + private boolean operator; + private boolean banned; + private boolean playedBefore; + private boolean online; + private boolean whitelisted; + + // Proxy helper + private static Map lookup = new ConcurrentHashMap(); + + /** + * Constructor used by serialization. + */ + public SerializedOfflinePlayer() { + // Do nothing + } + + /** + * Initialize this serializable offline player from another player. + * @param offline - another player. + */ + public SerializedOfflinePlayer(OfflinePlayer offline) { + this.name = offline.getName(); + this.firstPlayed = offline.getFirstPlayed(); + this.lastPlayed = offline.getLastPlayed(); + this.operator = offline.isOp(); + this.banned = offline.isBanned(); + this.playedBefore = offline.hasPlayedBefore(); + this.online = offline.isOnline(); + this.whitelisted = offline.isWhitelisted(); + } + + @Override + public boolean isOp() { + return operator; + } + + @Override + public void setOp(boolean operator) { + this.operator = operator; + } + + @Override + public Map serialize() { + throw new UnsupportedOperationException(); + } + + @Override + public Location getBedSpawnLocation() { + return bedSpawnLocation; + } + + @Override + public long getFirstPlayed() { + return firstPlayed; + } + + @Override + public long getLastPlayed() { + return lastPlayed; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasPlayedBefore() { + return playedBefore; + } + + @Override + public boolean isBanned() { + return banned; + } + + @Override + public void setBanned(boolean banned) { + this.banned = banned; + } + + @Override + public boolean isOnline() { + return online; + } + + @Override + public boolean isWhitelisted() { + return whitelisted; + } + + @Override + public void setWhitelisted(boolean whitelisted) { + this.whitelisted = whitelisted; + } + + private void writeObject(ObjectOutputStream output) throws IOException { + output.defaultWriteObject(); + + // Serialize the bed spawn location + output.writeUTF(bedSpawnLocation.getWorld().getName()); + output.writeDouble(bedSpawnLocation.getX()); + output.writeDouble(bedSpawnLocation.getY()); + output.writeDouble(bedSpawnLocation.getZ()); + } + + private void readObject(ObjectInputStream input) throws ClassNotFoundException, IOException { + input.defaultReadObject(); + + // Well, this is a problem + bedSpawnLocation = new Location( + getWorld(input.readUTF()), + input.readDouble(), + input.readDouble(), + input.readDouble() + ); + } + + private World getWorld(String name) { + try { + // Try to get the world at least + return Bukkit.getServer().getWorld(name); + } catch (Exception e) { + // Screw it + return null; + } + } + + @Override + public Player getPlayer() { + try { + // Try to get the real player underneath + return Bukkit.getServer().getPlayerExact(name); + } catch (Exception e) { + return getProxyPlayer(); + } + } + + /** + * Retrieve a player object that implements OfflinePlayer by refering to this object. + *

+ * All other methods cause an exception. + * @return Proxy object. + */ + public Player getProxyPlayer() { + + // Remember to initialize the method filter + if (lookup.size() == 0) { + // Add all public methods + for (Method method : OfflinePlayer.class.getMethods()) { + lookup.put(method.getName(), method); + } + } + + // MORE CGLIB magic! + Enhancer ex = new Enhancer(); + ex.setSuperclass(Player.class); + ex.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + // There's no overloaded methods, so we don't care + Method offlineMethod = lookup.get(method.getName()); + + // Ignore all other methods + if (offlineMethod == null) { + throw new UnsupportedOperationException( + "The method " + method.getName() + " is not supported for offline players."); + } + + // Invoke our on method + return offlineMethod.invoke(SerializedOfflinePlayer.this, args); + } + }); + + return (Player) ex.create(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java b/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java deleted file mode 100644 index 385e4d8c..00000000 --- a/ProtocolLib/src/com/comphenix/protocol/injector/ConcurrentListenerMultimap.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.comphenix.protocol.injector; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.comphenix.protocol.concurrency.SortedCopyOnWriteArray; -import com.comphenix.protocol.events.ListenerPriority; -import com.comphenix.protocol.events.ListeningWhitelist; -import com.comphenix.protocol.events.PacketAdapter; -import com.comphenix.protocol.events.PacketEvent; -import com.comphenix.protocol.events.PacketListener; -import com.google.common.base.Objects; -import com.google.common.primitives.Ints; - -/** - * A thread-safe implementation of a listener multimap. - * - * @author Kristian - */ -public class ConcurrentListenerMultimap { - - // The core of our map - protected ConcurrentMap> listeners = - new ConcurrentHashMap>(); - - /** - * Adds a listener to its requested list of packet recievers. - * @param listener - listener with a list of packets to recieve notifcations for. - * @param whitelist - the packet whitelist to use. - */ - public void addListener(PacketListener listener, ListeningWhitelist whitelist) { - - PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority()); - - for (Integer packetID : whitelist.getWhitelist()) { - addListener(packetID, prioritized); - } - } - - // Add the listener to a specific packet notifcation list - private void addListener(Integer packetID, PrioritizedListener listener) { - - SortedCopyOnWriteArray list = listeners.get(packetID); - - // We don't want to create this for every lookup - if (list == null) { - // It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order, - // which is a essential feature for our purposes. - final SortedCopyOnWriteArray value = new SortedCopyOnWriteArray(); - - list = listeners.putIfAbsent(packetID, value); - - // We may end up creating multiple multisets, but we'll agree - // on the one to use. - if (list == null) { - list = value; - } - } - - // Thread safe - list.add(listener); - } - - /** - * Removes the given listener from the packet event list. - * @param listener - listener to remove. - * @param whitelist - the packet whitelist that was used. - * @return Every packet ID that was removed due to no listeners. - */ - public List removeListener(PacketListener listener, ListeningWhitelist whitelist) { - - List removedPackets = new ArrayList(); - - // Again, not terribly efficient. But adding or removing listeners should be a rare event. - for (Integer packetID : whitelist.getWhitelist()) { - - SortedCopyOnWriteArray list = listeners.get(packetID); - - // Remove any listeners - if (list != null) { - // Don't remove from newly created lists - if (list.size() > 0) { - // Remove this listener. Note that priority is generally ignored. - list.remove(new PrioritizedListener(listener, whitelist.getPriority())); - - if (list.size() == 0) { - listeners.remove(packetID); - removedPackets.add(packetID); - } - } - } - - // Move on to the next - } - - return removedPackets; - } - - /** - * Invokes the given packet event for every registered listener. - * @param logger - the logger that will be used to inform about listener exceptions. - * @param event - the packet event to invoke. - */ - public void invokePacketRecieving(Logger logger, PacketEvent event) { - SortedCopyOnWriteArray list = listeners.get(event.getPacketID()); - - if (list == null) - return; - - // We have to be careful. Cannot modify the underlying list when sending notifications. - synchronized (list) { - for (PrioritizedListener element : list) { - try { - element.getListener().onPacketReceiving(event); - } catch (Throwable e) { - // Minecraft doesn't want your Exception. - logger.log(Level.SEVERE, - "Exception occured in onPacketReceiving() for " + - PacketAdapter.getPluginName(element.getListener()), e); - } - } - } - } - - /** - * Invokes the given packet event for every registered listener. - * @param logger - the logger that will be used to inform about listener exceptions. - * @param event - the packet event to invoke. - */ - public void invokePacketSending(Logger logger, PacketEvent event) { - SortedCopyOnWriteArray list = listeners.get(event.getPacketID()); - - if (list == null) - return; - - synchronized (list) { - for (PrioritizedListener element : list) { - try { - element.getListener().onPacketSending(event); - } catch (Throwable e) { - // Minecraft doesn't want your Exception. - logger.log(Level.SEVERE, - "Exception occured in onPacketReceiving() for " + - PacketAdapter.getPluginName(element.getListener()), e); - } - } - } - } - - /** - * A listener with an associated priority. - */ - private class PrioritizedListener implements Comparable { - private PacketListener listener; - private ListenerPriority priority; - - public PrioritizedListener(PacketListener listener, ListenerPriority priority) { - this.listener = listener; - this.priority = priority; - } - - @Override - public int compareTo(PrioritizedListener other) { - // This ensures that lower priority listeners are executed first - return Ints.compare(this.getPriority().getSlot(), - other.getPriority().getSlot()); - } - - // Note that this equals() method is NOT consistent with compareTo(). - // But, it's a private class so who cares. - @Override - public boolean equals(Object obj) { - // We only care about the listener - priority itself should not make a difference - if(obj instanceof PrioritizedListener){ - final PrioritizedListener other = (PrioritizedListener) obj; - return Objects.equal(listener, other.listener); - } else { - return false; - } - } - - public PacketListener getListener() { - return listener; - } - - public ListenerPriority getPriority() { - return priority; - } - } -} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java b/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java index d358bcd7..e3c4a2c9 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java @@ -35,8 +35,39 @@ class EntityUtilities { private static Method hashGetMethod; private static Method scanPlayersMethod; + /* + * While this function may look pretty bad, it's essentially just a reflection-warped + * version of the following: + * + * @SuppressWarnings("unchecked") + * public static void updateEntity2(Entity entity, List observers) { + * + * World world = entity.getWorld(); + * WorldServer worldServer = ((CraftWorld) world).getHandle(); + * + * EntityTracker tracker = worldServer.tracker; + * EntityTrackerEntry entry = (EntityTrackerEntry) tracker.trackedEntities.get(entity.getEntityId()); + * + * List nmsPlayers = getNmsPlayers(observers); + * + * entry.trackedPlayers.removeAll(nmsPlayers); + * entry.scanPlayers(nmsPlayers); + * } + * + * private static List getNmsPlayers(List players) { + * List nsmPlayers = new ArrayList(); + * + * for (Player bukkitPlayer : players) { + * CraftPlayer craftPlayer = (CraftPlayer) bukkitPlayer; + * nsmPlayers.add(craftPlayer.getHandle()); + * } + * + * return nsmPlayers; + * } + * + */ public static void updateEntity(Entity entity, List observers) throws FieldAccessException { - + World world = entity.getWorld(); Object worldServer = ((CraftWorld) world).getHandle(); diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java b/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java new file mode 100644 index 00000000..ea2576db --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/InjectedServerConnection.java @@ -0,0 +1,189 @@ +package com.comphenix.protocol.injector; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Server; + +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; + +/** + * Used to ensure that the 1.3 server is referencing the correct server handler. + * + * @author Kristian + */ +class InjectedServerConnection { + + private static Field listenerThreadField; + private static Field minecraftServerField; + private static Method serverConnectionMethod; + private static Field listField; + + private List listFields; + private List> replacedLists; + + private Server server; + private Logger logger; + private boolean hasAttempted; + private boolean hasSuccess; + + private Object minecraftServer = null; + + public InjectedServerConnection(Logger logger, Server server) { + this.listFields = new ArrayList(); + this.replacedLists = new ArrayList>(); + this.logger = logger; + this.server = server; + } + + public void injectList() { + + // Only execute this method once + if (!hasAttempted) + hasAttempted = true; + else + return; + + if (minecraftServerField == null) + minecraftServerField = FuzzyReflection.fromObject(server, true).getFieldByType(".*MinecraftServer"); + + try { + minecraftServer = FieldUtils.readField(minecraftServerField, server, true); + } catch (IllegalAccessException e1) { + logger.log(Level.WARNING, "Cannot extract minecraft server from Bukkit."); + return; + } + + try { + if (serverConnectionMethod == null) + serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()). + getMethodByParameters("getServerConnection", ".*ServerConnection", new String[] {}); + // We're using Minecraft 1.3.1 + injectServerConnection(); + + } catch (RuntimeException e) { + + // Minecraft 1.2.5 or lower + injectListenerThread(); + } + } + + private void injectListenerThread() { + + try { + + if (listenerThreadField == null) + listenerThreadField = FuzzyReflection.fromClass(minecraftServerField.getType()). + getFieldByType(".*NetworkListenThread"); + } catch (RuntimeException e) { + logger.log(Level.SEVERE, "Cannot find listener thread in MinecraftServer."); + return; + } + + Object listenerThread = null; + + // Attempt to get the thread + try { + listenerThread = listenerThreadField.get(minecraftServer); + } catch (Exception e) { + logger.log(Level.WARNING, "Unable to read the listener thread."); + return; + } + + // Ok, great. Get every list field + List lists = FuzzyReflection.fromClass(listenerThreadField.getType()).getFieldListByType(List.class); + + for (Field list : lists) { + injectIntoList(listenerThread, list); + } + + hasSuccess = true; + } + + private void injectServerConnection() { + + Object serverConnection = null; + + // Careful - we might fail + try { + serverConnection = serverConnectionMethod.invoke(minecraftServer); + } catch (Exception ex) { + logger.log(Level.WARNING, "Unable to retrieve server connection", ex); + return; + } + + if (listField == null) + listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true). + getFieldByType("serverConnection", List.class); + injectIntoList(serverConnection, listField); + hasSuccess = true; + } + + @SuppressWarnings("unchecked") + private void injectIntoList(Object instance, Field field) { + VolatileField listFieldRef = new VolatileField(listField, instance, true); + List list = (List) listFieldRef.getValue(); + + // Careful not to inject twice + if (list instanceof ReplacedArrayList) { + replacedLists.add((ReplacedArrayList) list); + } else { + replacedLists.add(new ReplacedArrayList(list)); + listFieldRef.setValue(replacedLists.get(0)); + listFields.add(listFieldRef); + } + } + + /** + * Replace the server handler instance kept by the "keep alive" object. + * @param oldHandler - old server handler. + * @param newHandler - new, proxied server handler. + */ + public void replaceServerHandler(Object oldHandler, Object newHandler) { + if (!hasAttempted) { + injectList(); + } + + if (hasSuccess) { + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.addMapping(oldHandler, newHandler); + } + } + } + + /** + * Revert to the old vanilla server handler, if it has been replaced. + * @param oldHandler - old vanilla server handler. + */ + public void revertServerHandler(Object oldHandler) { + if (hasSuccess) { + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.removeMapping(oldHandler); + } + } + } + + /** + * Undoes everything. + */ + public void cleanupAll() { + if (replacedLists.size() > 0) { + // Repair the underlying lists + for (ReplacedArrayList replacedList : replacedLists) { + replacedList.revertAll(); + } + for (VolatileField field : listFields) { + field.revertValue(); + } + + listFields.clear(); + replacedLists.clear(); + } + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java index 173f7e0b..2e372e1e 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/MinecraftRegistry.java @@ -49,7 +49,7 @@ class MinecraftRegistry { // Initialize it, if we haven't already if (packetToID == null) { try { - Field packetsField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType("java\\.util\\.Map"); + Field packetsField = FuzzyReflection.fromClass(Packet.class, true).getFieldByType("packetsField", Map.class); packetToID = (Map) FieldUtils.readStaticField(packetsField, true); } catch (IllegalAccessException e) { @@ -88,8 +88,8 @@ class MinecraftRegistry { Map lookup = forceVanilla ? previousValues : overwrittenPackets; // Optimized lookup - if (lookup.containsKey(packetToID)) { - return lookup.get(packetToID); + if (lookup.containsKey(packetID)) { + return lookup.get(packetID); } // Will most likely not be used diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkFieldInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkFieldInjector.java index e3f6923d..56903e35 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkFieldInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkFieldInjector.java @@ -10,6 +10,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.bukkit.entity.Player; +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; @@ -48,7 +51,7 @@ class NetworkFieldInjector extends PlayerInjector { } @Override - protected void initialize() throws IllegalAccessException { + protected synchronized void initialize() throws IllegalAccessException { super.initialize(); // Get the sync field as well @@ -83,6 +86,15 @@ class NetworkFieldInjector extends PlayerInjector { } } + @Override + public void checkListener(PacketListener listener) { + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), + Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) { + throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners."); + } + } + @Override public void injectManager() { diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkObjectInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkObjectInjector.java index 02efd317..9091390d 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkObjectInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkObjectInjector.java @@ -11,6 +11,10 @@ import java.util.Set; import org.bukkit.entity.Player; +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.events.ListeningWhitelist; +import com.comphenix.protocol.events.PacketListener; + /** * Injection method that overrides the NetworkHandler itself, and it's sendPacket-method. * @@ -42,6 +46,15 @@ class NetworkObjectInjector extends PlayerInjector { } } + @Override + public void checkListener(PacketListener listener) { + // Unfortunately, we don't support chunk packets + if (ListeningWhitelist.containsAny(listener.getSendingWhitelist(), + Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK)) { + throw new IllegalStateException("The NETWORK_FIELD_INJECTOR hook doesn't support map chunk listeners."); + } + } + @Override public void injectManager() { diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java new file mode 100644 index 00000000..fb901a1a --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/NetworkServerInjector.java @@ -0,0 +1,181 @@ +package com.comphenix.protocol.injector; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Set; + +import net.minecraft.server.Packet; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.Factory; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.instances.CollectionGenerator; +import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.ExistingGenerator; +import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; + +/** + * Represents a player hook into the NetServerHandler class. + * + * @author Kristian + */ +public class NetworkServerInjector extends PlayerInjector { + + private static Method sendPacketMethod; + + private StructureModifier serverHandlerModifier; + private InjectedServerConnection serverInjection; + + public NetworkServerInjector(Player player, PacketFilterManager manager, + Set sendingFilters, InjectedServerConnection serverInjection) throws IllegalAccessException { + super(player, manager, sendingFilters); + this.serverInjection = serverInjection; + } + + @Override + protected void initialize() throws IllegalAccessException { + super.initialize(); + + // Get the send packet method! + if (hasInitialized) { + if (sendPacketMethod == null) + sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*"); + if (serverHandlerModifier == null) + serverHandlerModifier = new StructureModifier(serverHandler.getClass(), null, false); + } + } + + @Override + public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException { + Object serverDeleage = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); + + if (serverDeleage != null) { + try { + // Note that invocation target exception is a wrapper for a checked exception + sendPacketMethod.invoke(serverDeleage, packet); + + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw e; + } catch (IllegalAccessException e) { + throw new IllegalStateException("Unable to access send packet method.", e); + } + } else { + throw new IllegalStateException("Unable to load server handler. Cannot send packet."); + } + } + + @Override + public void injectManager() { + + if (serverHandlerRef == null) + throw new IllegalStateException("Cannot find server handler."); + // Don't inject twice + if (serverHandlerRef.getValue() instanceof Factory) + return; + + Class serverClass = serverHandler.getClass(); + + Enhancer ex = new Enhancer(); + ex.setClassLoader(manager.getClassLoader()); + ex.setSuperclass(serverClass); + ex.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + // The send packet method! + if (method.equals(sendPacketMethod)) { + Packet packet = (Packet) args[0]; + + if (packet != null) { + packet = handlePacketRecieved(packet); + + // A NULL packet indicate cancelling + if (packet != null) + args[0] = packet; + else + return null; + } + } + + // Call the method directly + return proxy.invokeSuper(obj, args); + } + }); + + // Use the existing field values when we create our copy + DefaultInstances serverInstances = DefaultInstances.fromArray( + ExistingGenerator.fromObjectFields(serverHandler), + PrimitiveGenerator.INSTANCE, + CollectionGenerator.INSTANCE); + + Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass); + serverInjection.replaceServerHandler(serverHandler, proxyObject); + + // Inject it now + if (proxyObject != null) { + copyTo(serverHandler, proxyObject); + serverHandlerRef.setValue(proxyObject); + } else { + throw new RuntimeException( + "Cannot hook player: Unable to find a valid constructor for the NetServerHandler object."); + } + } + + /** + * Copy every field in server handler A to server handler B. + * @param source - fields to copy. + * @param destination - fields to copy to. + */ + private void copyTo(Object source, Object destination) { + StructureModifier modifierSource = serverHandlerModifier.withTarget(source); + StructureModifier modifierDest = serverHandlerModifier.withTarget(destination); + + // Copy every field + try { + for (int i = 0; i < modifierSource.size(); i++) { + modifierDest.write(i, modifierSource.read(i)); + } + } catch (FieldAccessException e) { + throw new RuntimeException("Unable to copy fields from NetServerHandler.", e); + } + } + + @Override + public void cleanupAll() { + if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) { + copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue()); + serverHandlerRef.revertValue(); + } + + serverInjection.revertServerHandler(serverHandler); + + try { + if (getNetHandler() != null) { + // Restore packet listener + try { + FieldUtils.writeField(netHandlerField, networkManager, serverHandlerRef.getOldValue(), true); + } catch (IllegalAccessException e) { + // Oh well + e.printStackTrace(); + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + @Override + public void checkListener(PacketListener listener) { + // We support everything + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java index cd380899..e9009586 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java @@ -34,6 +34,7 @@ import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; +import org.bukkit.Server; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -45,7 +46,10 @@ import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; +import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.async.AsyncFilterManager; +import com.comphenix.protocol.async.AsyncMarker; import com.comphenix.protocol.events.*; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; @@ -60,14 +64,23 @@ public final class PacketFilterManager implements ProtocolManager { */ public enum PlayerInjectHooks { /** - * Override the packet queue lists in NetworkHandler. + * Override the packet queue lists in NetworkHandler. + *

+ * Cannot intercept MapChunk packets. */ NETWORK_HANDLER_FIELDS, /** - * Override the network handler object itself. + * Override the network handler object itself. Only works in 1.3. + *

+ * Cannot intercept MapChunk packets. */ - NETWORK_MANAGER_OBJECT + NETWORK_MANAGER_OBJECT, + + /** + * Override the server handler object. Versatile, but a tad slower. + */ + NETWORK_SERVER_OBJECT; } // Create a concurrent set @@ -84,26 +97,35 @@ public final class PacketFilterManager implements ProtocolManager { // Packet injection private PacketInjector packetInjector; + // Server connection injection + private InjectedServerConnection serverInjection; + // Enabled packet filters private Set sendingFilters = Collections.newSetFromMap(new ConcurrentHashMap()); // The two listener containers - private ConcurrentListenerMultimap recievedListeners = new ConcurrentListenerMultimap(); - private ConcurrentListenerMultimap sendingListeners = new ConcurrentListenerMultimap(); + private SortedPacketListenerList recievedListeners = new SortedPacketListenerList(); + private SortedPacketListenerList sendingListeners = new SortedPacketListenerList(); // Whether or not this class has been closed - private boolean hasClosed; + private volatile boolean hasClosed; // The default class loader private ClassLoader classLoader; + // The last successful player hook + private PlayerInjector lastSuccessfulHook; + // Error logger private Logger logger; + // The async packet handler + private AsyncFilterManager asyncFilterManager; + /** * Only create instances of this class if protocol lib is disabled. */ - public PacketFilterManager(ClassLoader classLoader, Logger logger) { + public PacketFilterManager(ClassLoader classLoader, Server server, Logger logger) { if (logger == null) throw new IllegalArgumentException("logger cannot be NULL."); if (classLoader == null) @@ -114,11 +136,18 @@ public final class PacketFilterManager implements ProtocolManager { this.classLoader = classLoader; this.logger = logger; this.packetInjector = new PacketInjector(classLoader, this, connectionLookup); + this.asyncFilterManager = new AsyncFilterManager(logger, server.getScheduler(), this); + this.serverInjection = new InjectedServerConnection(logger, server); } catch (IllegalAccessException e) { logger.log(Level.SEVERE, "Unable to initialize packet injector.", e); } } + @Override + public AsynchronousManager getAsynchronousManager() { + return asyncFilterManager; + } + /** * Retrieves how the server packets are read. * @return Injection method for reading server packets. @@ -133,6 +162,13 @@ public final class PacketFilterManager implements ProtocolManager { */ public void setPlayerHook(PlayerInjectHooks playerHook) { this.playerHook = playerHook; + + // Make sure the current listeners are compatible + if (lastSuccessfulHook != null) { + for (PacketListener listener : packetListeners) { + checkListener(listener); + } + } } public Logger getLogger() { @@ -161,12 +197,17 @@ public final class PacketFilterManager implements ProtocolManager { if (hasSending || hasReceiving) { // Add listeners and hooks if (hasSending) { + verifyWhitelist(listener, sending); sendingListeners.addListener(listener, sending); enablePacketFilters(ConnectionSide.SERVER_SIDE, sending.getWhitelist()); } if (hasReceiving) { + verifyWhitelist(listener, receiving); recievedListeners.addListener(listener, receiving); enablePacketFilters(ConnectionSide.CLIENT_SIDE, receiving.getWhitelist()); + + // We don't know if we've hooked any players yet + checkListener(listener); } // Inform our injected hooks @@ -174,6 +215,36 @@ public final class PacketFilterManager implements ProtocolManager { } } + /** + * Determine if the packet IDs in a whitelist is valid. + * @param listener - the listener that will be mentioned in the error. + * @param whitelist - whitelist of packet IDs. + * @throws IllegalArgumentException If the whitelist is illegal. + */ + public static void verifyWhitelist(PacketListener listener, ListeningWhitelist whitelist) { + for (Integer id : whitelist.getWhitelist()) { + if (id >= 256 || id < 0) { + throw new IllegalArgumentException(String.format("Invalid packet id %s in listener %s.", + id, PacketAdapter.getPluginName(listener)) + ); + } + } + } + + /** + * Determine if a listener is valid or not. + * @param listener - listener to check. + * @throws IllegalStateException If the given listener's whitelist cannot be fulfilled. + */ + public void checkListener(PacketListener listener) { + try { + if (lastSuccessfulHook != null) + lastSuccessfulHook.checkListener(listener); + } catch (Exception e) { + throw new IllegalStateException("Registering listener " + PacketAdapter.getPluginName(listener) + " failed", e); + } + } + @Override public void removePacketListener(PacketListener listener) { if (listener == null) @@ -212,6 +283,9 @@ public final class PacketFilterManager implements ProtocolManager { removePacketListener(listener); } } + + // Do the same for the asynchronous events + asyncFilterManager.unregisterAsyncHandlers(plugin); } /** @@ -219,7 +293,7 @@ public final class PacketFilterManager implements ProtocolManager { * @param event - the packet event to invoke. */ public void invokePacketRecieving(PacketEvent event) { - recievedListeners.invokePacketRecieving(logger, event); + handlePacket(recievedListeners, event, false); } /** @@ -227,7 +301,41 @@ public final class PacketFilterManager implements ProtocolManager { * @param event - the packet event to invoke. */ public void invokePacketSending(PacketEvent event) { - sendingListeners.invokePacketSending(logger, event); + handlePacket(sendingListeners, event, true); + } + + /** + * Handle a packet sending or receiving event. + *

+ * Note that we also handle asynchronous events. + * @param packetListeners - packet listeners that will receive this event. + * @param event - the evnet to broadcast. + */ + private void handlePacket(SortedPacketListenerList packetListeners, PacketEvent event, boolean sending) { + + // By default, asynchronous packets are queued for processing + if (asyncFilterManager.hasAsynchronousListeners(event)) { + event.setAsyncMarker(asyncFilterManager.createAsyncMarker()); + } + + // Process synchronous events + if (sending) + packetListeners.invokePacketSending(logger, event); + else + packetListeners.invokePacketRecieving(logger, event); + + // To cancel asynchronous processing, use the async marker + if (!event.isCancelled() && !hasAsyncCancelled(event.getAsyncMarker())) { + asyncFilterManager.enqueueSyncPacket(event, event.getAsyncMarker()); + + // The above makes a copy of the event, so it's safe to cancel it + event.setCancelled(true); + } + } + + // NULL marker mean we're dealing with no asynchronous listeners + private boolean hasAsyncCancelled(AsyncMarker marker) { + return marker == null || marker.isAsyncCancelled(); } /** @@ -298,6 +406,9 @@ public final class PacketFilterManager implements ProtocolManager { PlayerInjector injector = getInjector(sender); Packet mcPacket = packet.getHandle(); + // Make sure the packet isn't cancelled + packetInjector.undoCancel(packet.getID(), mcPacket); + if (filters) { mcPacket = injector.handlePacketRecieved(mcPacket); } @@ -343,8 +454,6 @@ public final class PacketFilterManager implements ProtocolManager { @Override public void updateEntity(Entity entity, List observers) throws FieldAccessException { - - EntityUtilities.updateEntity(entity, observers); } @@ -360,20 +469,22 @@ public final class PacketFilterManager implements ProtocolManager { /** * Used to construct a player hook. * @param player - the player to hook. + * @param hook - the hook type. * @return A new player hoook * @throws IllegalAccessException Unable to do our reflection magic. */ - protected PlayerInjector getPlayerHookInstance(Player player) throws IllegalAccessException { - + protected PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException { // Construct the correct player hook - switch (playerHook) { + switch (hook) { case NETWORK_HANDLER_FIELDS: return new NetworkFieldInjector(player, this, sendingFilters); case NETWORK_MANAGER_OBJECT: return new NetworkObjectInjector(player, this, sendingFilters); + case NETWORK_SERVER_OBJECT: + return new NetworkServerInjector(player, this, sendingFilters, serverInjection); + default: + throw new IllegalArgumentException("Cannot construct a player injector."); } - - throw new IllegalArgumentException("Cannot construct a player injector."); } /** @@ -381,20 +492,51 @@ public final class PacketFilterManager implements ProtocolManager { * @param player - player to hook. */ protected void injectPlayer(Player player) { + + PlayerInjector injector = null; + PlayerInjectHooks currentHook = playerHook; + boolean firstPlayer = lastSuccessfulHook == null; + // Don't inject if the class has closed if (!hasClosed && player != null && !playerInjection.containsKey(player)) { - try { - PlayerInjector injector = getPlayerHookInstance(player); + while (true) { + try { + injector = getHookInstance(player, currentHook); + injector.injectManager(); + playerInjection.put(player, injector); + connectionLookup.put(injector.getInputStream(false), player); + break; + + } catch (Exception e) { - injector.injectManager(); - playerInjection.put(player, injector); - connectionLookup.put(injector.getInputStream(false), player); - - } catch (IllegalAccessException e) { - // Mark this injection attempt as a failure - playerInjection.put(player, null); - logger.log(Level.SEVERE, "Unable to access fields.", e); + // Mark this injection attempt as a failure + logger.log(Level.SEVERE, "Player hook " + currentHook.toString() + " failed.", e); + + // Clean up as much as possible + try { + if (injector != null) + injector.cleanupAll(); + } catch (Exception e2) { + logger.log(Level.WARNING, "Cleaing up after player hook failed.", e); + } + + if (currentHook.ordinal() > 0) { + // Choose the previous player hook type + currentHook = PlayerInjectHooks.values()[currentHook.ordinal() - 1]; + logger.log(Level.INFO, "Switching to " + currentHook.toString() + " instead."); + } else { + // UTTER FAILURE + playerInjection.put(player, null); + return; + } + } } + + // Update values + if (injector != null) + lastSuccessfulHook = injector; + if (currentHook != playerHook || firstPlayer) + setPlayerHook(currentHook); } } @@ -477,7 +619,7 @@ public final class PacketFilterManager implements ProtocolManager { if (event instanceof PlayerJoinEvent) injectPlayer(((PlayerJoinEvent) event).getPlayer()); else if (event instanceof PlayerQuitEvent) - injectPlayer(((PlayerQuitEvent) event).getPlayer()); + uninjectPlayer(((PlayerQuitEvent) event).getPlayer()); } return null; } @@ -524,12 +666,12 @@ public final class PacketFilterManager implements ProtocolManager { if (!hasClosed && player != null) { PlayerInjector injector = playerInjection.get(player); - DataInputStream input = injector.getInputStream(true); if (injector != null) { + DataInputStream input = injector.getInputStream(true); injector.cleanupAll(); - playerInjection.remove(injector); + playerInjection.remove(player); connectionLookup.remove(input); } } @@ -565,23 +707,31 @@ public final class PacketFilterManager implements ProtocolManager { public void close() { // Guard - if (hasClosed) + if (hasClosed || playerInjection == null) return; - + // Remove everything for (PlayerInjector injection : playerInjection.values()) { - injection.cleanupAll(); + if (injection != null) { + injection.cleanupAll(); + } } - + // Remove packet handlers if (packetInjector != null) packetInjector.cleanupAll(); + // Remove server handler + serverInjection.cleanupAll(); + hasClosed = true; + // Remove listeners packetListeners.clear(); playerInjection.clear(); connectionLookup.clear(); - hasClosed = true; + + // Clean up async handlers. We have to do this last. + asyncFilterManager.cleanupAll(); } @Override diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java index ebdfd578..998eb5a7 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketInjector.java @@ -23,6 +23,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.entity.Player; @@ -52,6 +53,9 @@ class PacketInjector { // Allows us to determine the sender private Map playerLookup; + // Allows us to look up read packet injectors + private Map readModifier; + // Class loader private ClassLoader classLoader; @@ -61,9 +65,24 @@ class PacketInjector { this.classLoader = classLoader; this.manager = manager; this.playerLookup = playerLookup; + this.readModifier = new ConcurrentHashMap(); initialize(); } + /** + * Undo a packet cancel. + * @param id - the id of the packet. + * @param packet - packet to uncancel. + */ + public void undoCancel(Integer id, Packet packet) { + ReadPacketModifier modifier = readModifier.get(id); + + // Cancelled packets are represented with NULL + if (modifier != null && modifier.getOverride(packet) == null) { + modifier.removeOverride(packet); + } + } + private void initialize() throws IllegalAccessException { if (intHashMap == null) { // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. @@ -109,10 +128,12 @@ class PacketInjector { ex.setClassLoader(classLoader); Class proxy = ex.createClass(); + // Create the proxy handler + ReadPacketModifier modifier = new ReadPacketModifier(packetID, this); + readModifier.put(packetID, modifier); + // Add a static reference - Enhancer.registerStaticCallbacks(proxy, new Callback[] { - new ReadPacketModifier(packetID, this) - }); + Enhancer.registerStaticCallbacks(proxy, new Callback[] { modifier }); try { // Override values @@ -147,6 +168,7 @@ class PacketInjector { putMethod.invoke(intHashMap, packetID, old); previous.remove(packetID); + readModifier.remove(packetID); registry.remove(proxy); overwritten.remove(packetID); return true; diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java index 79842052..2ae2976d 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PlayerInjector.java @@ -31,6 +31,7 @@ import org.bukkit.entity.Player; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; @@ -56,9 +57,11 @@ abstract class PlayerInjector { // Reference to the player's network manager protected VolatileField networkManagerRef; + protected VolatileField serverHandlerRef; protected Object networkManager; // Current net handler + protected Object serverHandler; protected Object netHandler; // The packet manager and filters @@ -75,12 +78,18 @@ abstract class PlayerInjector { initialize(); } + /** + * Retrieve the notch (NMS) entity player object. + * @return Notch player object. + */ + protected EntityPlayer getEntityPlayer() { + CraftPlayer craft = (CraftPlayer) player; + return craft.getHandle(); + } + protected void initialize() throws IllegalAccessException { - CraftPlayer craft = (CraftPlayer) player; - EntityPlayer notchEntity = craft.getHandle(); - - Object serverHandler = null; + EntityPlayer notchEntity = getEntityPlayer(); if (!hasInitialized) { // Do this first, in case we encounter an exception @@ -89,7 +98,8 @@ abstract class PlayerInjector { // Retrieve the server handler if (serverHandlerField == null) serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType(".*NetServerHandler"); - serverHandler = FieldUtils.readField(serverHandlerField, notchEntity); + serverHandlerRef = new VolatileField(serverHandlerField, notchEntity); + serverHandler = serverHandlerRef.getValue(); // Next, get the network manager if (networkManagerField == null) @@ -118,26 +128,31 @@ abstract class PlayerInjector { * @return Current net handler. * @throws IllegalAccessException Unable to find or retrieve net handler. */ - private Object getNetHandler() throws IllegalAccessException { + protected Object getNetHandler() throws IllegalAccessException { // What a mess try { if (netHandlerField == null) - netHandlerField = FuzzyReflection.fromClass(networkManagerField.getType(), true). + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). getFieldByType("net\\.minecraft\\.NetHandler"); } catch (RuntimeException e1) { + // Swallow it + } + + // Second attempt + if (netHandlerField == null) { try { // Well, that sucks. Try just Minecraft objects then. - netHandlerField = FuzzyReflection.fromClass(networkManagerField.getType(), true). + netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). getFieldByType(FuzzyReflection.MINECRAFT_OBJECT); } catch (RuntimeException e2) { - return new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); + throw new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); } } // Get the handler - if (netHandler != null) + if (netHandler == null) netHandler = FieldUtils.readField(netHandlerField, networkManager, true); return netHandler; } @@ -190,6 +205,14 @@ abstract class PlayerInjector { */ public abstract void cleanupAll(); + /** + * Invoked before a new listener is registered. + *

+ * The player injector should throw an exception if this listener cannot be properly supplied with packet events. + * @param listener - the listener that is about to be registered. + */ + public abstract void checkListener(PacketListener listener); + /** * Allows a packet to be recieved by the listeners. * @param packet - packet to recieve. @@ -200,7 +223,7 @@ abstract class PlayerInjector { Integer id = MinecraftRegistry.getPacketToID().get(packet.getClass()); // Make sure we're listening - if (sendingFilters.contains(id)) { + if (id != null && sendingFilters.contains(id)) { // A packet has been sent guys! PacketContainer container = new PacketContainer(id, packet); PacketEvent event = PacketEvent.fromServer(manager, container, player); diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PrioritizedListener.java b/ProtocolLib/src/com/comphenix/protocol/injector/PrioritizedListener.java new file mode 100644 index 00000000..2d971206 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PrioritizedListener.java @@ -0,0 +1,64 @@ +package com.comphenix.protocol.injector; + +import com.comphenix.protocol.events.ListenerPriority; +import com.google.common.base.Objects; +import com.google.common.primitives.Ints; + +/** + * Represents a listener with a priority. + * + * @author Kristian + */ +public class PrioritizedListener implements Comparable> { + + private TListener listener; + private ListenerPriority priority; + + public PrioritizedListener(TListener listener, ListenerPriority priority) { + this.listener = listener; + this.priority = priority; + } + + @Override + public int compareTo(PrioritizedListener other) { + // This ensures that lower priority listeners are executed first + return Ints.compare( + this.getPriority().getSlot(), + other.getPriority().getSlot()); + } + + // Note that this equals() method is NOT consistent with compareTo(). + // But, it's a private class so who cares. + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object obj) { + // We only care about the listener - priority itself should not make a difference + if(obj instanceof PrioritizedListener){ + final PrioritizedListener other = (PrioritizedListener) obj; + return Objects.equal(listener, other.listener); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(listener); + } + + /** + * Retrieve the underlying listener. + * @return Underlying listener. + */ + public TListener getListener() { + return listener; + } + + /** + * Retrieve the priority of this listener. + * @return Listener priority. + */ + public ListenerPriority getPriority() { + return priority; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java b/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java index d8400125..d5b84bbe 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/ReadPacketModifier.java @@ -45,6 +45,23 @@ class ReadPacketModifier implements MethodInterceptor { this.packetID = packetID; this.packetInjector = packetInjector; } + + /** + * Remove any packet overrides. + * @param packet - the packet to rever + */ + public void removeOverride(Packet packet) { + override.remove(packet); + } + + /** + * Retrieve the packet that overrides the methods of the given packet. + * @param packet - the given packet. + * @return Overriden object. + */ + public Object getOverride(Packet packet) { + return override.get(packet); + } @Override public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { @@ -60,9 +77,14 @@ class ReadPacketModifier implements MethodInterceptor { if (override.containsKey(thisObj)) { Object overridenObject = override.get(thisObj); - // Cancel EVERYTHING, including "processPacket" - if (overridenObject == null) - return null; + // This packet has been cancelled + if (overridenObject == null) { + // So, cancel all void methods + if (method.getReturnType().equals(Void.TYPE)) + return null; + else // Revert to normal for everything else + overridenObject = thisObj; + } returnValue = proxy.invokeSuper(overridenObject, args); } else { @@ -75,7 +97,7 @@ class ReadPacketModifier implements MethodInterceptor { // We need this in order to get the correct player DataInputStream input = (DataInputStream) args[0]; - + // Let the people know PacketContainer container = new PacketContainer(packetID, (Packet) thisObj); PacketEvent event = packetInjector.packetRecieved(container, input); diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java b/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java new file mode 100644 index 00000000..64e3b0de --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/ReplacedArrayList.java @@ -0,0 +1,136 @@ +package com.comphenix.protocol.injector; + +import java.util.Collection; +import java.util.List; + +import com.google.common.base.Objects; +import com.google.common.collect.BiMap; +import com.google.common.collect.ForwardingList; +import com.google.common.collect.HashBiMap; + +/** + * Represents an array list that wraps another list, while automatically replacing one element with another. + *

+ * The replaced elements can be recovered. + * + * @author Kristian + * @param - type of the elements we're replacing. + */ +class ReplacedArrayList extends ForwardingList { + private BiMap replaceMap = HashBiMap.create(); + private List underlyingList; + + public ReplacedArrayList(List underlyingList) { + this.underlyingList = underlyingList; + } + + @Override + public boolean add(TKey element) { + if (replaceMap.containsKey(element)) { + return super.add(replaceMap.get(element)); + } else { + return super.add(element); + } + } + + @Override + public void add(int index, TKey element) { + if (replaceMap.containsKey(element)) { + super.add(index, replaceMap.get(element)); + } else { + super.add(index, element); + } + } + + @Override + public boolean addAll(Collection collection) { + int oldSize = size(); + + for (TKey element : collection) + add(element); + return size() != oldSize; + } + + @Override + public boolean addAll(int index, Collection elements) { + int oldSize = size(); + + for (TKey element : elements) + add(index++, element); + return size() != oldSize; + } + + @Override + protected List delegate() { + return underlyingList; + } + + /** + * Add a replace rule. + *

+ * This automatically replaces every existing element. + * @param target - instance to find. + * @param replacement - instance to replace with. + */ + public synchronized void addMapping(TKey target, TKey replacement) { + replaceMap.put(target, replacement); + + // Replace existing elements + replaceAll(target, replacement); + } + + /** + * Revert the given mapping. + * @param target - the instance we replaced. + */ + public synchronized void removeMapping(TKey target) { + // Make sure the mapping exist + if (replaceMap.containsKey(target)) { + TKey replacement = replaceMap.get(target); + replaceMap.remove(target); + + // Revert existing elements + replaceAll(replacement, target); + } + } + + /** + * Replace all instances of the given object. + * @param find - object to find. + * @param replace - object to replace it with. + */ + public synchronized void replaceAll(TKey find, TKey replace) { + for (int i = 0; i < underlyingList.size(); i++) { + if (Objects.equal(underlyingList.get(i), find)) + underlyingList.set(i, replace); + } + } + + /** + * Undo all replacements. + */ + public synchronized void revertAll() { + + // No need to do anything else + if (replaceMap.size() < 1) + return; + + BiMap inverse = replaceMap.inverse(); + + for (int i = 0; i < underlyingList.size(); i++) { + TKey replaced = underlyingList.get(i); + + if (inverse.containsKey(replaced)) { + underlyingList.set(i, inverse.get(replaced)); + } + } + + replaceMap.clear(); + } + + @Override + protected void finalize() throws Throwable { + revertAll(); + super.finalize(); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/SortedPacketListenerList.java b/ProtocolLib/src/com/comphenix/protocol/injector/SortedPacketListenerList.java new file mode 100644 index 00000000..7ff8cddd --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/SortedPacketListenerList.java @@ -0,0 +1,66 @@ +package com.comphenix.protocol.injector; + +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.events.PacketListener; + +/** + * Registry of synchronous packet listeners. + * + * @author Kristian + */ +class SortedPacketListenerList extends AbstractConcurrentListenerMultimap { + + /** + * Invokes the given packet event for every registered listener. + * @param logger - the logger that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + */ + public void invokePacketRecieving(Logger logger, PacketEvent event) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + // The returned list is thread-safe + for (PrioritizedListener element : list) { + try { + element.getListener().onPacketReceiving(event); + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + logger.log(Level.SEVERE, + "Exception occured in onPacketReceiving() for " + + PacketAdapter.getPluginName(element.getListener()), e); + } + } + } + + /** + * Invokes the given packet event for every registered listener. + * @param logger - the logger that will be used to inform about listener exceptions. + * @param event - the packet event to invoke. + */ + public void invokePacketSending(Logger logger, PacketEvent event) { + Collection> list = getListener(event.getPacketID()); + + if (list == null) + return; + + for (PrioritizedListener element : list) { + try { + element.getListener().onPacketSending(event); + } catch (Throwable e) { + // Minecraft doesn't want your Exception. + logger.log(Level.SEVERE, + "Exception occured in onPacketSending() for " + + PacketAdapter.getPluginName(element.getListener()), e); + } + } + } + +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java b/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java index bc80d438..6dff5dfc 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/StructureCache.java @@ -17,12 +17,17 @@ package com.comphenix.protocol.injector; -import java.util.HashMap; -import java.util.Map; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import net.minecraft.server.Packet; import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; +import com.comphenix.protocol.reflect.compiler.CompileListener; +import com.comphenix.protocol.reflect.compiler.CompiledStructureModifier; /** * Caches structure modifiers. @@ -30,7 +35,10 @@ import com.comphenix.protocol.reflect.StructureModifier; */ public class StructureCache { // Structure modifiers - private static Map> structureModifiers = new HashMap>(); + private static ConcurrentMap> structureModifiers = + new ConcurrentHashMap>(); + + private static Set compiling = new HashSet(); /** * Creates an empty Minecraft packet of the given ID. @@ -53,15 +61,51 @@ public class StructureCache { * @return A structure modifier. */ public static StructureModifier getStructure(int id) { + // Compile structures by default + return getStructure(id, true); + } + + /** + * Retrieve a cached structure modifier for the given packet id. + * @param id - packet ID. + * @param compile - whether or not to asynchronously compile the structure modifier. + * @return A structure modifier. + */ + public static StructureModifier getStructure(int id, boolean compile) { StructureModifier result = structureModifiers.get(id); - - // Use the vanilla class definition + + // We don't want to create this for every lookup if (result == null) { - result = new StructureModifier( + // Use the vanilla class definition + final StructureModifier value = new StructureModifier( MinecraftRegistry.getPacketClassFromID(id, true), Packet.class, true); - structureModifiers.put(id, result); + result = structureModifiers.putIfAbsent(id, value); + + // We may end up creating multiple modifiers, but we'll agree on which to use + if (result == null) { + result = value; + } + } + + // Automatically compile the structure modifier + if (compile && !(result instanceof CompiledStructureModifier)) { + // Compilation is many orders of magnitude slower than synchronization + synchronized (compiling) { + final int idCopy = id; + final BackgroundCompiler compiler = BackgroundCompiler.getInstance(); + + if (!compiling.contains(id) && compiler != null) { + compiler.scheduleCompilation(result, new CompileListener() { + @Override + public void onCompiled(StructureModifier compiledModifier) { + structureModifiers.put(idCopy, compiledModifier); + } + }); + compiling.add(id); + } + } } return result; diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java index a8e3eb6b..448507f7 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -19,8 +19,10 @@ package com.comphenix.protocol.reflect; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; @@ -96,6 +98,7 @@ public class FuzzyReflection { * Retrieves a method by looking at its name. * @param nameRegex - regular expression that will match method names. * @return The first method that satisfies the regular expression. + * @throws RuntimeException If the method cannot be found. */ public Method getMethodByName(String nameRegex) { @@ -139,11 +142,38 @@ public class FuzzyReflection { * @return The first method that satisfies the parameter types. */ public Method getMethodByParameters(String name, Class returnType, Class[] args) { + // Find the correct method to call + List methods = getMethodListByParameters(returnType, args); + + if (methods.size() > 0) { + return methods.get(0); + } else { + // That sucks + throw new RuntimeException("Unable to find " + name + " in " + source.getName()); + } + } + /** + * Retrieves a method by looking at the parameter types and return type only. + * @param name - potential name of the method. Only used by the error mechanism. + * @param returnType - regular expression matching the return type of the method to find. + * @param args - regular expressions of the matching parameter types. + * @return The first method that satisfies the parameter types. + */ + public Method getMethodByParameters(String name, String returnTypeRegex, String[] argsRegex) { + + Pattern match = Pattern.compile(returnTypeRegex); + Pattern[] argMatch = new Pattern[argsRegex.length]; + + for (int i = 0; i < argsRegex.length; i++) { + argMatch[i] = Pattern.compile(argsRegex[i]); + } + // Find the correct method to call for (Method method : getMethods()) { - if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) { - return method; + if (match.matcher(method.getReturnType().getName()).matches()) { + if (matchParameters(argMatch, method.getParameterTypes())) + return method; } } @@ -151,6 +181,39 @@ public class FuzzyReflection { throw new RuntimeException("Unable to find " + name + " in " + source.getName()); } + private boolean matchParameters(Pattern[] parameterMatchers, Class[] argTypes) { + if (parameterMatchers.length != argTypes.length) + throw new IllegalArgumentException("Arrays must have the same cardinality."); + + // Check types against the regular expressions + for (int i = 0; i < argTypes.length; i++) { + if (!parameterMatchers[i].matcher(argTypes[i].getName()).matches()) + return false; + } + + return true; + } + + /** + * Retrieves every method that has the given parameter types and return type. + * @param returnType - return type of the method to find. + * @param args - parameter types of the method to find. + * @return Every method that satisfies the given constraints. + */ + public List getMethodListByParameters(Class returnType, Class[] args) { + + List methods = new ArrayList(); + + // Find the correct method to call + for (Method method : getMethods()) { + if (method.getReturnType().equals(returnType) && Arrays.equals(method.getParameterTypes(), args)) { + methods.add(method); + } + } + + return methods; + } + /** * Retrieves a field by name. * @param nameRegex - regular expression that will match a field name. @@ -172,6 +235,46 @@ public class FuzzyReflection { nameRegex + " in " + source.getName()); } + /** + * Retrieves the first field with a type equal to or more specific to the given type. + * @param name - name the field probably is given. This will only be used in the error message. + * @param type - type of the field to find. + * @return The first field with a type that is an instance of the given type. + */ + public Field getFieldByType(String name, Class type) { + + List fields = getFieldListByType(type); + + if (fields.size() > 0) { + return fields.get(0); + } else { + // Looks like we're outdated. Too bad. + throw new RuntimeException(String.format("Unable to find a field %s with the type %s in %s", + name, type.getName(), source.getName()) + ); + } + } + + /** + * Retrieves every field with a type equal to or more specific to the given type. + * @param type - type of the fields to find. + * @return Every field with a type that is an instance of the given type. + */ + public List getFieldListByType(Class type) { + + List fields = new ArrayList(); + + // Field with a compatible type + for (Field field : getFields()) { + // A assignable from B -> B instanceOf A + if (type.isAssignableFrom(field.getType())) { + fields.add(field); + } + } + + return fields; + } + /** * Retrieves a field by type. *

diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java index 0f9aed1f..416a7de4 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/StructureModifier.java @@ -21,34 +21,45 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +/** + * Provides list-oriented access to the fields of a Minecraft packet. + *

+ * Implemented by using reflection. Use a CompiledStructureModifier, if speed is essential. + * + * @author Kristian + * @param Type of the fields to retrieve. + */ @SuppressWarnings("rawtypes") public class StructureModifier { // Object and its type - private Class targetType; - private Object target; + protected Class targetType; + protected Object target; // Converter. May be NULL. - private EquivalentConverter converter; + protected EquivalentConverter converter; // The fields to read in order - private Class fieldType; - private List data = new ArrayList(); + protected Class fieldType; + protected List data = new ArrayList(); // Improved default values - private Set defaultFields; + protected Map defaultFields; // Cache of previous types - private Map subtypeCache; + protected Map subtypeCache; + + // Whether or subclasses should handle conversion + protected boolean customConvertHandling; /** * Creates a structure modifier. @@ -58,22 +69,38 @@ public class StructureModifier { */ public StructureModifier(Class targetType, Class superclassExclude, boolean requireDefault) { List fields = getFields(targetType, superclassExclude); - Set defaults = requireDefault ? generateDefaultFields(fields) : new HashSet(); + Map defaults = requireDefault ? generateDefaultFields(fields) : new HashMap(); - initialize(targetType, Object.class, fields, defaults, null, new HashMap()); + initialize(targetType, Object.class, fields, defaults, null, new ConcurrentHashMap()); } - private StructureModifier(StructureModifier other, Object target) { - initialize(other.targetType, other.fieldType, other.data, other.defaultFields, other.converter, other.subtypeCache); - this.target = target; + /** + * Consumers of this method should call "initialize". + */ + protected StructureModifier() { + } - private StructureModifier() { - // Consumers of this method should call "initialize" + /** + * Initialize using the same field types. + * @param other - information to set. + */ + protected void initialize(StructureModifier other) { + initialize(other.targetType, other.fieldType, other.data, + other.defaultFields, other.converter, other.subtypeCache); } - private void initialize(Class targetType, Class fieldType, - List data, Set defaultFields, + /** + * Initialize every field of this class. + * @param targetType - type of the object we're reading and writing from. + * @param fieldType - the common type of the fields we're modifying. + * @param data - list of fields to modify. + * @param defaultFields - list of fields that will be automatically initialized. + * @param converter - converts between the common field type and the actual type the consumer expects. + * @param subTypeCache - a structure modifier cache. + */ + protected void initialize(Class targetType, Class fieldType, + List data, Map defaultFields, EquivalentConverter converter, Map subTypeCache) { this.targetType = targetType; this.fieldType = fieldType; @@ -101,7 +128,7 @@ public class StructureModifier { Object result = FieldUtils.readField(data.get(fieldIndex), target, true); // Use the converter, if we have it - if (converter != null) + if (needConversion()) return converter.getSpecific(result); else return (TField) result; @@ -140,7 +167,7 @@ public class StructureModifier { throw new IllegalStateException("Cannot write to a NULL target."); // Use the converter, if it exists - Object obj = converter != null ? converter.getGeneric(value) : value; + Object obj = needConversion() ? converter.getGeneric(value) : value; try { FieldUtils.writeField(data.get(fieldIndex), target, obj, true); @@ -152,6 +179,14 @@ public class StructureModifier { return this; } + /** + * Whether or not we should use the converter instance. + * @return TRUE if we should, FALSE otherwise. + */ + private final boolean needConversion() { + return converter != null && !customConvertHandling; + } + /** * Writes the value of a given field IF and ONLY if it exists. * @param fieldIndex - index of the potential field. @@ -197,7 +232,7 @@ public class StructureModifier { DefaultInstances generator = DefaultInstances.DEFAULT; // Write a default instance to every field - for (Field field : defaultFields) { + for (Field field : defaultFields.keySet()) { try { FieldUtils.writeField(field, target, generator.getDefault(field.getType()), true); @@ -223,24 +258,32 @@ public class StructureModifier { // Do we need to update the cache? if (result == null) { List filtered = new ArrayList(); - Set defaults = new HashSet(); + Map defaults = new HashMap(); + int index = 0; for (Field field : data) { if (fieldType != null && fieldType.isAssignableFrom(field.getType())) { filtered.add(field); - if (defaultFields.contains(field)) - defaults.add(field); + // Don't use the original index + if (defaultFields.containsKey(field)) + defaults.put(field, index); } + + // Keep track of the field index + index++; } // Cache structure modifiers - result = new StructureModifier(); - result.initialize(targetType, fieldType, filtered, defaults, - converter, new HashMap()); + result = withFieldType(fieldType, filtered, defaults, converter); - if (fieldType != null) + if (fieldType != null) { subtypeCache.put(fieldType, result); + + // Automatically compile the structure modifier + if (BackgroundCompiler.getInstance() != null) + BackgroundCompiler.getInstance().scheduleCompilation(subtypeCache, fieldType); + } } // Add the target too @@ -296,28 +339,59 @@ public class StructureModifier { return data.size(); } + /** + * Create a new structure modifier for the new field type. + * @param fieldType - common type of each field. + * @param filtered - list of fields after filtering the original modifier. + * @param defaults - list of default values after filtering the original. + * @param converter - the new converter. + * @return A new structure modifier. + */ + protected StructureModifier withFieldType( + Class fieldType, List filtered, + Map defaults, EquivalentConverter converter) { + + StructureModifier result = new StructureModifier(); + result.initialize(targetType, fieldType, filtered, defaults, + converter, new ConcurrentHashMap()); + return result; + } + /** * Retrieves a structure modifier of the same type for a different object target. * @param target - different target of the same type. * @return Structure modifier with the new target. */ public StructureModifier withTarget(Object target) { - return new StructureModifier(this, target); + StructureModifier copy = new StructureModifier(); + + // Create a new instance + copy.initialize(targetType, fieldType, data, defaultFields, converter, subtypeCache); + copy.target = target; + return copy; } /** * Retrieves a structure modifier with the same type and target, but using a new object converter. - * @param converter- the object converter to use. + * @param converter - the object converter to use. * @return Structure modifier with the new converter. */ @SuppressWarnings("unchecked") private StructureModifier withConverter(EquivalentConverter converter) { - StructureModifier copy = new StructureModifier(this, target); + StructureModifier copy = withTarget(target); - copy.converter = converter; + copy.setConverter(converter); return copy; } + /** + * Set the current object converter. Should only be called during construction. + * @param converter - current object converter. + */ + protected void setConverter(EquivalentConverter converter) { + this.converter = converter; + } + /** * Retrieves a list of the fields matching the constraints of this structure modifier. * @return List of fields. @@ -325,7 +399,7 @@ public class StructureModifier { public List getFields() { return ImmutableList.copyOf(data); } - + /** * Retrieve every value stored in the fields of the current type. * @return Every field value. @@ -334,17 +408,19 @@ public class StructureModifier { public List getValues() throws FieldAccessException { List values = new ArrayList(); - for (int i = 0; i < size(); i++) + for (int i = 0; i < size(); i++) { values.add(read(i)); + } return values; } // Used to generate plausible default values - private static Set generateDefaultFields(List fields) { + private static Map generateDefaultFields(List fields) { - Set requireDefaults = new HashSet(); + Map requireDefaults = new HashMap(); DefaultInstances generator = DefaultInstances.DEFAULT; + int index = 0; for (Field field : fields) { Class type = field.getType(); @@ -354,9 +430,12 @@ public class StructureModifier { // Next, see if we actually can generate a default value if (generator.getDefault(type) != null) { // If so, require it - requireDefaults.add(field); + requireDefaults.put(field, index); } } + + // Increment field index + index++; } return requireDefaults; diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java index 4e22f7ef..64272b96 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/VolatileField.java @@ -155,6 +155,13 @@ public class VolatileField { } } + /** + * Determine whether or not we'll need to revert the value. + */ + public boolean isCurrentSet() { + return currentSet; + } + private void ensureLoaded() { // Load the value if we haven't already if (!previousLoaded) { diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java new file mode 100644 index 00000000..61a42d48 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -0,0 +1,183 @@ +package com.comphenix.protocol.reflect.compiler; + +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.comphenix.protocol.reflect.StructureModifier; + +/** + * Compiles structure modifiers on a background thread. + *

+ * This is necessary as we cannot block the main thread. + * + * @author Kristian + */ +public class BackgroundCompiler { + + // How long to wait for a shutdown + public static final int SHUTDOWN_DELAY_MS = 2000; + + // The single background compiler we're using + private static BackgroundCompiler backgroundCompiler; + + private StructureCompiler compiler; + private boolean enabled; + private boolean shuttingDown; + + private ExecutorService executor; + + /** + * Retrieves the current background compiler. + * @return Current background compiler. + */ + public static BackgroundCompiler getInstance() { + return backgroundCompiler; + } + + /** + * Sets the single background compiler we're using. + * @param backgroundCompiler - current background compiler, or NULL if the library is not loaded. + */ + public static void setInstance(BackgroundCompiler backgroundCompiler) { + BackgroundCompiler.backgroundCompiler = backgroundCompiler; + } + + /** + * Initialize a background compiler. + * @param loader - class loader from Bukkit. + */ + public BackgroundCompiler(ClassLoader loader) { + this(loader, Executors.newSingleThreadExecutor()); + } + + /** + * Initialize a background compiler utilizing the given thread pool. + * @param loader - class loader from Bukkit. + * @param executor - thread pool we'll use. + */ + public BackgroundCompiler(ClassLoader loader, ExecutorService executor) { + if (loader == null) + throw new IllegalArgumentException("loader cannot be NULL"); + if (executor == null) + throw new IllegalArgumentException("executor cannot be NULL"); + + this.compiler = new StructureCompiler(loader); + this.executor = executor; + this.enabled = true; + } + + /** + * Ensure that the indirectly given structure modifier is eventually compiled. + * @param cache - store of structure modifiers. + * @param key - key of the structure modifier to compile. + */ + @SuppressWarnings("rawtypes") + public void scheduleCompilation(final Map cache, final Class key) { + + @SuppressWarnings("unchecked") + final StructureModifier uncompiled = cache.get(key); + + if (uncompiled != null) { + scheduleCompilation(uncompiled, new CompileListener() { + @Override + public void onCompiled(StructureModifier compiledModifier) { + // Update cache + cache.put(key, compiledModifier); + } + }); + } + } + + /** + * Ensure that the given structure modifier is eventually compiled. + * @param uncompiled - structure modifier to compile. + * @param listener - listener responsible for responding to the compilation. + */ + public void scheduleCompilation(final StructureModifier uncompiled, final CompileListener listener) { + + // Only schedule if we're enabled + if (enabled && !shuttingDown) { + + // Don't try to schedule anything + if (executor == null || executor.isShutdown()) + return; + + try { + executor.submit(new Callable() { + @Override + public Object call() throws Exception { + + StructureModifier modifier = uncompiled; + + // Do our compilation + modifier = compiler.compile(modifier); + listener.onCompiled(modifier); + + // We'll also return the new structure modifier + return modifier; + } + }); + } catch (RejectedExecutionException e) { + // Occures when the underlying queue is overflowing. Since the compilation + // is only an optmization and not really essential we'll just log this failure + // and move on. + Logger.getLogger("Minecraft").log(Level.WARNING, "Unable to schedule compilation task.", e); + } + } + } + + /** + * Clean up after ourselves using the default timeout. + */ + public void shutdownAll() { + shutdownAll(SHUTDOWN_DELAY_MS, TimeUnit.MILLISECONDS); + } + + /** + * Clean up after ourselves. + * @param timeout - the maximum time to wait. + * @param unit - the time unit of the timeout argument. + */ + public void shutdownAll(long timeout, TimeUnit unit) { + setEnabled(false); + shuttingDown = true; + executor.shutdown(); + + try { + executor.awaitTermination(timeout, unit); + } catch (InterruptedException e) { + // Unlikely to ever occur. + e.printStackTrace(); + } + } + + /** + * Retrieve whether or not the background compiler is enabled. + * @return TRUE if it is enabled, FALSE otherwise. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether or not the background compiler is enabled. + * @param enabled - TRUE to enable it, FALSE otherwise. + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Retrieve the current structure compiler. + * @return Current structure compiler. + */ + public StructureCompiler getCompiler() { + return compiler; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BoxingHelper.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BoxingHelper.java new file mode 100644 index 00000000..06d043a1 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/BoxingHelper.java @@ -0,0 +1,275 @@ +package com.comphenix.protocol.reflect.compiler; + +import net.sf.cglib.asm.*; + +/** + * Used by the compiler to automatically box and unbox values. + */ +class BoxingHelper { + + private final static Type BYTE_TYPE = Type.getObjectType("java/lang/Byte"); + private final static Type BOOLEAN_TYPE = Type.getObjectType("java/lang/Boolean"); + private final static Type SHORT_TYPE = Type.getObjectType("java/lang/Short"); + private final static Type CHARACTER_TYPE = Type.getObjectType("java/lang/Character"); + private final static Type INTEGER_TYPE = Type.getObjectType("java/lang/Integer"); + private final static Type FLOAT_TYPE = Type.getObjectType("java/lang/Float"); + private final static Type LONG_TYPE = Type.getObjectType("java/lang/Long"); + private final static Type DOUBLE_TYPE = Type.getObjectType("java/lang/Double"); + private final static Type NUMBER_TYPE = Type.getObjectType("java/lang/Number"); + private final static Type OBJECT_TYPE = Type.getObjectType("java/lang/Object"); + + private final static MethodDescriptor BOOLEAN_VALUE = MethodDescriptor.getMethod("boolean booleanValue()"); + private final static MethodDescriptor CHAR_VALUE = MethodDescriptor.getMethod("char charValue()"); + private final static MethodDescriptor INT_VALUE = MethodDescriptor.getMethod("int intValue()"); + private final static MethodDescriptor FLOAT_VALUE = MethodDescriptor.getMethod("float floatValue()"); + private final static MethodDescriptor LONG_VALUE = MethodDescriptor.getMethod("long longValue()"); + private final static MethodDescriptor DOUBLE_VALUE = MethodDescriptor.getMethod("double doubleValue()"); + + private MethodVisitor mv; + + public BoxingHelper(MethodVisitor mv) { + this.mv = mv; + } + + /** + * Generates the instructions to box the top stack value. This value is + * replaced by its boxed equivalent on top of the stack. + * + * @param type the type of the top stack value. + */ + public void box(final Type type){ + if(type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) { + return; + } + + if(type == Type.VOID_TYPE) { + push((String) null); + } else { + Type boxed = type; + + switch(type.getSort()) { + case Type.BYTE: + boxed = BYTE_TYPE; + break; + case Type.BOOLEAN: + boxed = BOOLEAN_TYPE; + break; + case Type.SHORT: + boxed = SHORT_TYPE; + break; + case Type.CHAR: + boxed = CHARACTER_TYPE; + break; + case Type.INT: + boxed = INTEGER_TYPE; + break; + case Type.FLOAT: + boxed = FLOAT_TYPE; + break; + case Type.LONG: + boxed = LONG_TYPE; + break; + case Type.DOUBLE: + boxed = DOUBLE_TYPE; + break; + } + + newInstance(boxed); + if(type.getSize() == 2) { + // Pp -> Ppo -> oPpo -> ooPpo -> ooPp -> o + dupX2(); + dupX2(); + pop(); + } else { + // p -> po -> opo -> oop -> o + dupX1(); + swap(); + } + + invokeConstructor(boxed, new MethodDescriptor("", Type.VOID_TYPE, new Type[] {type})); + } + } + + /** + * Generates the instruction to invoke a constructor. + * + * @param type the class in which the constructor is defined. + * @param method the constructor to be invoked. + */ + public void invokeConstructor(final Type type, final MethodDescriptor method){ + invokeInsn(Opcodes.INVOKESPECIAL, type, method); + } + + /** + * Generates a DUP_X1 instruction. + */ + public void dupX1(){ + mv.visitInsn(Opcodes.DUP_X1); + } + + /** + * Generates a DUP_X2 instruction. + */ + public void dupX2(){ + mv.visitInsn(Opcodes.DUP_X2); + } + + /** + * Generates a POP instruction. + */ + public void pop(){ + mv.visitInsn(Opcodes.POP); + } + + /** + * Generates a SWAP instruction. + */ + public void swap(){ + mv.visitInsn(Opcodes.SWAP); + } + + /** + * Generates the instruction to push the given value on the stack. + * + * @param value the value to be pushed on the stack. + */ + public void push(final boolean value){ + push(value ? 1 : 0); + } + + /** + * Generates the instruction to push the given value on the stack. + * + * @param value the value to be pushed on the stack. + */ + public void push(final int value) { + if (value >= -1 && value <= 5) { + mv.visitInsn(Opcodes.ICONST_0 + value); + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + mv.visitIntInsn(Opcodes.BIPUSH, value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + mv.visitIntInsn(Opcodes.SIPUSH, value); + } else { + mv.visitLdcInsn(new Integer(value)); + } + } + + /** + * Generates the instruction to create a new object. + * + * @param type the class of the object to be created. + */ + public void newInstance(final Type type){ + typeInsn(Opcodes.NEW, type); + } + + /** + * Generates the instruction to push the given value on the stack. + * + * @param value the value to be pushed on the stack. May be null. + */ + public void push(final String value) { + if (value == null) { + mv.visitInsn(Opcodes.ACONST_NULL); + } else { + mv.visitLdcInsn(value); + } + } + + /** + * Generates the instructions to unbox the top stack value. This value is + * replaced by its unboxed equivalent on top of the stack. + * + * @param type + * the type of the top stack value. + */ + public void unbox(final Type type){ + Type t = NUMBER_TYPE; + MethodDescriptor sig = null; + + switch(type.getSort()) { + case Type.VOID: + return; + case Type.CHAR: + t = CHARACTER_TYPE; + sig = CHAR_VALUE; + break; + case Type.BOOLEAN: + t = BOOLEAN_TYPE; + sig = BOOLEAN_VALUE; + break; + case Type.DOUBLE: + sig = DOUBLE_VALUE; + break; + case Type.FLOAT: + sig = FLOAT_VALUE; + break; + case Type.LONG: + sig = LONG_VALUE; + break; + case Type.INT: + case Type.SHORT: + case Type.BYTE: + sig = INT_VALUE; + } + + if(sig == null) { + checkCast(type); + } else { + checkCast(t); + invokeVirtual(t, sig); + } + } + + /** + * Generates the instruction to check that the top stack value is of the + * given type. + * + * @param type a class or interface type. + */ + public void checkCast(final Type type){ + if(!type.equals(OBJECT_TYPE)) { + typeInsn(Opcodes.CHECKCAST, type); + } + } + + /** + * Generates the instruction to invoke a normal method. + * + * @param owner the class in which the method is defined. + * @param method the method to be invoked. + */ + public void invokeVirtual(final Type owner, final MethodDescriptor method){ + invokeInsn(Opcodes.INVOKEVIRTUAL, owner, method); + } + + /** + * Generates an invoke method instruction. + * + * @param opcode the instruction's opcode. + * @param type the class in which the method is defined. + * @param method the method to be invoked. + */ + private void invokeInsn(final int opcode, final Type type, final MethodDescriptor method){ + String owner = type.getSort() == Type.ARRAY ? type.getDescriptor() : type.getInternalName(); + mv.visitMethodInsn(opcode, owner, method.getName(), method.getDescriptor()); + } + + /** + * Generates a type dependent instruction. + * + * @param opcode the instruction's opcode. + * @param type the instruction's operand. + */ + private void typeInsn(final int opcode, final Type type){ + String desc; + + if(type.getSort() == Type.ARRAY) { + desc = type.getDescriptor(); + } else { + desc = type.getInternalName(); + } + + mv.visitTypeInsn(opcode, desc); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompileListener.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompileListener.java new file mode 100644 index 00000000..5ab92978 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompileListener.java @@ -0,0 +1,17 @@ +package com.comphenix.protocol.reflect.compiler; + +import com.comphenix.protocol.reflect.StructureModifier; + +/** + * Used to save the result of an compilation. + * + * @author Kristian + * @param - type of the structure modifier field. + */ +public interface CompileListener { + /** + * Invoked when a structure modifier has been successfully compiled. + * @param compiledModifier - the compiled structure modifier. + */ + public void onCompiled(StructureModifier compiledModifier); +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java new file mode 100644 index 00000000..d96236f7 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/CompiledStructureModifier.java @@ -0,0 +1,73 @@ +package com.comphenix.protocol.reflect.compiler; + +import java.lang.reflect.Field; +import java.util.Map; + +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.instances.DefaultInstances; + +/** + * Represents a compiled structure modifier. + * + * @author Kristian + * @param Field type. + */ +public abstract class CompiledStructureModifier extends StructureModifier { + // Used to compile instances of structure modifiers + protected StructureCompiler compiler; + + public CompiledStructureModifier() { + super(); + customConvertHandling = true; + } + + // Speed up the default writer + @SuppressWarnings("unchecked") + @Override + public StructureModifier writeDefaults() throws FieldAccessException { + + DefaultInstances generator = DefaultInstances.DEFAULT; + + // Write a default instance to every field + for (Map.Entry entry : defaultFields.entrySet()) { + Integer index = entry.getValue(); + Field field = entry.getKey(); + + write(index, (TField) generator.getDefault(field.getType())); + } + + return this; + } + + @SuppressWarnings("unchecked") + @Override + public final TField read(int fieldIndex) throws FieldAccessException { + Object result = readGenerated(fieldIndex); + + if (converter != null) + return converter.getSpecific(result); + else + return (TField) result; + } + + protected abstract Object readGenerated(int fieldIndex) throws FieldAccessException; + + @SuppressWarnings("unchecked") + @Override + public StructureModifier write(int index, Object value) throws FieldAccessException { + if (converter != null) + value = converter.getGeneric((TField) value); + return writeGenerated(index, value); + } + + protected abstract StructureModifier writeGenerated(int index, Object value) throws FieldAccessException; + + @Override + public StructureModifier withTarget(Object target) { + if (compiler != null) + return compiler.compile(super.withTarget(target)); + else + return super.withTarget(target); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/MethodDescriptor.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/MethodDescriptor.java new file mode 100644 index 00000000..b1aa1260 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/MethodDescriptor.java @@ -0,0 +1,220 @@ +package com.comphenix.protocol.reflect.compiler; + +import java.util.HashMap; +import java.util.Map; + +import net.sf.cglib.asm.Type; + +/** + * Represents a method. + */ +class MethodDescriptor { + + /** + * The method name. + */ + private final String name; + + /** + * The method descriptor. + */ + private final String desc; + + /** + * Maps primitive Java type names to their descriptors. + */ + private static final Map DESCRIPTORS; + + static { + DESCRIPTORS = new HashMap(); + DESCRIPTORS.put("void", "V"); + DESCRIPTORS.put("byte", "B"); + DESCRIPTORS.put("char", "C"); + DESCRIPTORS.put("double", "D"); + DESCRIPTORS.put("float", "F"); + DESCRIPTORS.put("int", "I"); + DESCRIPTORS.put("long", "J"); + DESCRIPTORS.put("short", "S"); + DESCRIPTORS.put("boolean", "Z"); + } + + /** + * Creates a new {@link Method}. + * + * @param name the method's name. + * @param desc the method's descriptor. + */ + public MethodDescriptor(final String name, final String desc) { + this.name = name; + this.desc = desc; + } + + /** + * Creates a new {@link Method}. + * + * @param name the method's name. + * @param returnType the method's return type. + * @param argumentTypes the method's argument types. + */ + public MethodDescriptor( + final String name, + final Type returnType, + final Type[] argumentTypes) + { + this(name, Type.getMethodDescriptor(returnType, argumentTypes)); + } + + /** + * Returns a {@link Method} corresponding to the given Java method + * declaration. + * + * @param method a Java method declaration, without argument names, of the + * form "returnType name (argumentType1, ... argumentTypeN)", where + * the types are in plain Java (e.g. "int", "float", + * "java.util.List", ...). Classes of the java.lang package can be + * specified by their unqualified name; all other classes names must + * be fully qualified. + * @return a {@link Method} corresponding to the given Java method + * declaration. + * @throws IllegalArgumentException if method could not get + * parsed. + */ + public static MethodDescriptor getMethod(final String method) + throws IllegalArgumentException + { + return getMethod(method, false); + } + + /** + * Returns a {@link Method} corresponding to the given Java method + * declaration. + * + * @param method a Java method declaration, without argument names, of the + * form "returnType name (argumentType1, ... argumentTypeN)", where + * the types are in plain Java (e.g. "int", "float", + * "java.util.List", ...). Classes of the java.lang package may be + * specified by their unqualified name, depending on the + * defaultPackage argument; all other classes names must be fully + * qualified. + * @param defaultPackage true if unqualified class names belong to the + * default package, or false if they correspond to java.lang classes. + * For instance "Object" means "Object" if this option is true, or + * "java.lang.Object" otherwise. + * @return a {@link Method} corresponding to the given Java method + * declaration. + * @throws IllegalArgumentException if method could not get + * parsed. + */ + public static MethodDescriptor getMethod( + final String method, + final boolean defaultPackage) throws IllegalArgumentException + { + int space = method.indexOf(' '); + int start = method.indexOf('(', space) + 1; + int end = method.indexOf(')', start); + if (space == -1 || start == -1 || end == -1) { + throw new IllegalArgumentException(); + } + String returnType = method.substring(0, space); + String methodName = method.substring(space + 1, start - 1).trim(); + StringBuffer sb = new StringBuffer(); + sb.append('('); + int p; + do { + String s; + p = method.indexOf(',', start); + if (p == -1) { + s = map(method.substring(start, end).trim(), defaultPackage); + } else { + s = map(method.substring(start, p).trim(), defaultPackage); + start = p + 1; + } + sb.append(s); + } while (p != -1); + sb.append(')'); + sb.append(map(returnType, defaultPackage)); + return new MethodDescriptor(methodName, sb.toString()); + } + + private static String map(final String type, final boolean defaultPackage) { + if ("".equals(type)) { + return type; + } + + StringBuffer sb = new StringBuffer(); + int index = 0; + while ((index = type.indexOf("[]", index) + 1) > 0) { + sb.append('['); + } + + String t = type.substring(0, type.length() - sb.length() * 2); + String desc = (String) DESCRIPTORS.get(t); + if (desc != null) { + sb.append(desc); + } else { + sb.append('L'); + if (t.indexOf('.') < 0) { + if (!defaultPackage) { + sb.append("java/lang/"); + } + sb.append(t); + } else { + sb.append(t.replace('.', '/')); + } + sb.append(';'); + } + return sb.toString(); + } + + /** + * Returns the name of the method described by this object. + * + * @return the name of the method described by this object. + */ + public String getName() { + return name; + } + + /** + * Returns the descriptor of the method described by this object. + * + * @return the descriptor of the method described by this object. + */ + public String getDescriptor() { + return desc; + } + + /** + * Returns the return type of the method described by this object. + * + * @return the return type of the method described by this object. + */ + public Type getReturnType() { + return Type.getReturnType(desc); + } + + /** + * Returns the argument types of the method described by this object. + * + * @return the argument types of the method described by this object. + */ + public Type[] getArgumentTypes() { + return Type.getArgumentTypes(desc); + } + + public String toString() { + return name + desc; + } + + public boolean equals(final Object o) { + if (!(o instanceof MethodDescriptor)) { + return false; + } + MethodDescriptor other = (MethodDescriptor) o; + return name.equals(other.name) && desc.equals(other.desc); + } + + public int hashCode() { + return name.hashCode() ^ desc.hashCode(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/StructureCompiler.java b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/StructureCompiler.java new file mode 100644 index 00000000..24477dff --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/compiler/StructureCompiler.java @@ -0,0 +1,443 @@ +package com.comphenix.protocol.reflect.compiler; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.comphenix.protocol.reflect.PrimitiveUtils; +import com.comphenix.protocol.reflect.StructureModifier; +import com.google.common.base.Objects; + +import net.sf.cglib.asm.*; + +// public class CompiledStructureModifierPacket20 extends CompiledStructureModifier { +// +// private Packet20NamedEntitySpawn typedTarget; +// +// public CompiledStructureModifierPacket20(StructureModifier other, StructureCompiler compiler) { +// super(); +// initialize(other); +// this.target = other.getTarget(); +// this.typedTarget = (Packet20NamedEntitySpawn) target; +// this.compiler = compiler; +// } +// +// @Override +// protected Object readGenerated(int fieldIndex) throws FieldAccessException { +// +// Packet20NamedEntitySpawn target = typedTarget; +// +// switch (fieldIndex) { +// case 0: return (Object) target.a; +// case 1: return (Object) target.b; +// case 2: return (Object) target.c; +// case 3: return super.read(fieldIndex); +// case 4: return super.read(fieldIndex); +// case 5: return (Object) target.f; +// case 6: return (Object) target.g; +// case 7: return (Object) target.h; +// default: +// throw new FieldAccessException("Invalid index " + fieldIndex); +// } +// } +// +// @Override +// protected StructureModifier writeGenerated(int index, Object value) throws FieldAccessException { +// +// Packet20NamedEntitySpawn target = typedTarget; +// +// switch (index) { +// case 0: target.a = (Integer) value; break; +// case 1: target.b = (String) value; break; +// case 2: target.c = (Integer) value; break; +// case 3: target.d = (Integer) value; break; +// case 4: super.write(index, value); break; +// case 5: super.write(index, value); break; +// case 6: target.g = (Byte) value; break; +// case 7: target.h = (Integer) value; break; +// default: +// throw new FieldAccessException("Invalid index " + index); +// } +// +// // Chaining +// return this; +// } +// } + +/** + * Represents a StructureModifier compiler. + * + * @author Kristian + */ +public final class StructureCompiler { + + // Used to store generated classes of different types + @SuppressWarnings("rawtypes") + private class StructureKey { + private Class targetType; + private Class fieldType; + + public StructureKey(Class targetType, Class fieldType) { + this.targetType = targetType; + this.fieldType = fieldType; + } + + @Override + public int hashCode() { + return Objects.hashCode(targetType, fieldType); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StructureKey) { + StructureKey other = (StructureKey) obj; + return Objects.equal(targetType, other.targetType) && + Objects.equal(fieldType, other.fieldType); + } + return false; + } + } + + // Used to load classes + private volatile static Method defineMethod; + + @SuppressWarnings("rawtypes") + private Map compiledCache = new HashMap(); + + // The class loader we'll store our classes + private ClassLoader loader; + + // References to other classes + private static String PACKAGE_NAME = "com/comphenix/protocol/reflect/compiler"; + private static String SUPER_CLASS = "com/comphenix/protocol/reflect/StructureModifier"; + private static String COMPILED_CLASS = PACKAGE_NAME + "/CompiledStructureModifier"; + private static String FIELD_EXCEPTION_CLASS = "com/comphenix/protocol/reflect/FieldAccessException"; + + /** + * Construct a structure compiler. + * @param loader - main class loader. + */ + StructureCompiler(ClassLoader loader) { + this.loader = loader; + } + + /** + * Compiles the given structure modifier. + *

+ * WARNING: Do NOT call this method in the main thread. Compiling may easily take 10 ms, which is already + * over 1/4 of a tick (50 ms). Let the background thread automatically compile the structure modifiers instead. + * @param source - structure modifier to compile. + * @return A compiled structure modifier. + */ + @SuppressWarnings("unchecked") + public synchronized StructureModifier compile(StructureModifier source) { + + // We cannot optimize a structure modifier with no public fields + if (!isAnyPublic(source.getFields())) { + return source; + } + + StructureKey key = new StructureKey(source.getTargetType(), source.getFieldType()); + Class compiledClass = compiledCache.get(key); + + if (!compiledCache.containsKey(key)) { + compiledClass = generateClass(source); + compiledCache.put(key, compiledClass); + } + + // Next, create an instance of this class + try { + return (StructureModifier) compiledClass.getConstructor( + StructureModifier.class, StructureCompiler.class). + newInstance(source, this); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Used invalid parameters in instance creation", e); + } catch (SecurityException e) { + throw new RuntimeException("Security limitation!", e); + } catch (InstantiationException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot create instance of dynamic class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured while instancing generated class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Cannot happen.", e); + } + } + + private Class generateClass(StructureModifier source) { + + ClassWriter cw = new ClassWriter(0); + + @SuppressWarnings("rawtypes") + Class targetType = source.getTargetType(); + + String className = "CompiledStructure$" + targetType.getSimpleName() + source.getFieldType().getSimpleName(); + String targetSignature = Type.getDescriptor(targetType); + String targetName = targetType.getName().replace('.', '/'); + + try { + // This class might have been generated before. Try to load it. + Class before = loader.loadClass(PACKAGE_NAME.replace('/', '.') + "." + className); + + if (before != null) + return before; + } catch (ClassNotFoundException e) { + // That's ok. + } + + cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, PACKAGE_NAME + "/" + className, + "L" + COMPILED_CLASS + ";", + COMPILED_CLASS, null); + + createFields(cw, targetSignature); + createConstructor(cw, className, targetSignature, targetName); + createReadMethod(cw, className, source.getFields(), targetSignature, targetName); + createWriteMethod(cw, className, source.getFields(), targetSignature, targetName); + cw.visitEnd(); + + byte[] data = cw.toByteArray(); + + // Call the define method + try { + if (defineMethod == null) { + Method defined = ClassLoader.class.getDeclaredMethod("defineClass", + new Class[] { String.class, byte[].class, int.class, int.class }); + + // Awesome. Now, create and return it. + defined.setAccessible(true); + defineMethod = defined; + } + + @SuppressWarnings("rawtypes") + Class clazz = (Class) defineMethod.invoke(loader, null, data, 0, data.length); + + // DEBUG CODE: Print the content of the generated class. + //org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(data); + //cr.accept(new ASMifierClassVisitor(new PrintWriter(System.out)), 0); + + return clazz; + + } catch (SecurityException e) { + throw new RuntimeException("Cannot use reflection to dynamically load a class.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Incompatible JVM.", e); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Cannot call defineMethod - wrong JVM?", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Security limitation! Cannot dynamically load class.", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error occured in code generator.", e); + } + } + + /** + * Determine if at least one of the given fields is public. + * @param fields - field to test. + * @return TRUE if one or more field is publically accessible, FALSE otherwise. + */ + private boolean isAnyPublic(List fields) { + // Are any of the fields public? + for (int i = 0; i < fields.size(); i++) { + if (isPublic(fields.get(i))) { + return true; + } + } + + return false; + } + + private boolean isPublic(Field field) { + return Modifier.isPublic(field.getModifiers()); + } + + private void createFields(ClassWriter cw, String targetSignature) { + FieldVisitor typedField = cw.visitField(Opcodes.ACC_PRIVATE, "typedTarget", targetSignature, null, null); + typedField.visitEnd(); + } + + private void createWriteMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + + String methodDescriptor = "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"; + String methodSignature = "(ITTField;)L" + SUPER_CLASS + ";"; + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "writeGenerated", methodDescriptor, methodSignature, + new String[] { FIELD_EXCEPTION_CLASS }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 3); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + Label returnLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, labels.length - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + + Class outputType = fields.get(i).getType(); + Class inputType = PrimitiveUtils.wrap(outputType); + String typeDescriptor = Type.getDescriptor(outputType); + String inputPath = inputType.getName().replace('.', '/'); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Only write to public fields + if (isPublic(fields.get(i))) { + mv.visitVarInsn(Opcodes.ALOAD, 3); + mv.visitVarInsn(Opcodes.ALOAD, 2); + + if (!PrimitiveUtils.isPrimitive(outputType)) + mv.visitTypeInsn(Opcodes.CHECKCAST, inputPath); + else + boxingHelper.unbox(Type.getType(outputType)); + + mv.visitFieldInsn(Opcodes.PUTFIELD, targetName, fields.get(i).getName(), typeDescriptor); + + } else { + // Use reflection. We don't have a choice, unfortunately. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "write", "(ILjava/lang/Object;)L" + SUPER_CLASS + ";"); + mv.visitInsn(Opcodes.POP); + } + + mv.visitJumpInsn(Opcodes.GOTO, returnLabel); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + + mv.visitLabel(returnLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(5, 4); + mv.visitEnd(); + } + + private void createReadMethod(ClassWriter cw, String className, List fields, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PROTECTED, "readGenerated", "(I)Ljava/lang/Object;", null, + new String[] { "com/comphenix/protocol/reflect/FieldAccessException" }); + BoxingHelper boxingHelper = new BoxingHelper(mv); + + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ASTORE, 2); + mv.visitVarInsn(Opcodes.ILOAD, 1); + + // The last label is for the default switch + Label[] labels = new Label[fields.size()]; + Label errorLabel = new Label(); + + // Generate labels + for (int i = 0; i < fields.size(); i++) { + labels[i] = new Label(); + } + + mv.visitTableSwitchInsn(0, fields.size() - 1, errorLabel, labels); + + for (int i = 0; i < fields.size(); i++) { + Class outputType = fields.get(i).getType(); + String typeDescriptor = Type.getDescriptor(outputType); + + mv.visitLabel(labels[i]); + + // Push the compare object + if (i == 0) + mv.visitFrame(Opcodes.F_APPEND, 1, new Object[] { targetName }, 0, null); + else + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + + // Note that byte code cannot access non-public fields + if (isPublic(fields.get(i))) { + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.GETFIELD, targetName, fields.get(i).getName(), typeDescriptor); + + boxingHelper.box(Type.getType(outputType)); + } else { + // We have to use reflection for private and protected fields. + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "read", "(I)Ljava/lang/Object;"); + } + + mv.visitInsn(Opcodes.ARETURN); + } + + mv.visitLabel(errorLabel); + mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); + mv.visitTypeInsn(Opcodes.NEW, FIELD_EXCEPTION_CLASS); + mv.visitInsn(Opcodes.DUP); + mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("Invalid index "); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "(Ljava/lang/String;)V"); + mv.visitVarInsn(Opcodes.ILOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, FIELD_EXCEPTION_CLASS, "", "(Ljava/lang/String;)V"); + mv.visitInsn(Opcodes.ATHROW); + mv.visitMaxs(5, 3); + mv.visitEnd(); + } + + private void createConstructor(ClassWriter cw, String className, String targetSignature, String targetName) { + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", + "(L" + SUPER_CLASS + ";L" + PACKAGE_NAME + "/StructureCompiler;)V", + "(L" + SUPER_CLASS + ";L" + SUPER_CLASS + ";)V", null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, COMPILED_CLASS, "", "()V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, PACKAGE_NAME + "/" + className, "initialize", "(L" + SUPER_CLASS + ";)V"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, SUPER_CLASS, "getTarget", "()Ljava/lang/Object;"); + mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;"); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitFieldInsn(Opcodes.GETFIELD, PACKAGE_NAME + "/" + className, "target", "Ljava/lang/Object;"); + mv.visitTypeInsn(Opcodes.CHECKCAST, targetName); + mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "typedTarget", targetSignature); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitVarInsn(Opcodes.ALOAD, 2); + mv.visitFieldInsn(Opcodes.PUTFIELD, PACKAGE_NAME + "/" + className, "compiler", "L" + PACKAGE_NAME + "/StructureCompiler;"); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 3); + mv.visitEnd(); + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/com/comphenix/protocol/reflect/instances/DefaultInstances.java index 6ab5c1dd..7b846077 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -20,6 +20,8 @@ package com.comphenix.protocol.reflect.instances; import java.lang.reflect.Constructor; import java.util.*; +import net.sf.cglib.proxy.Enhancer; + import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; @@ -100,6 +102,38 @@ public class DefaultInstances { return getDefaultInternal(type, registered, 0); } + /** + * Retrieve the constructor with the fewest number of parameters. + * @param type - type to construct. + * @return A constructor with the fewest number of parameters, or NULL if the type has no constructors. + */ + @SuppressWarnings("unchecked") + public Constructor getMinimumConstructor(Class type) { + + Constructor minimum = null; + int lastCount = Integer.MAX_VALUE; + + // Find the constructor with the fewest parameters + for (Constructor candidate : type.getConstructors()) { + Class[] types = candidate.getParameterTypes(); + + // Note that we don't allow recursive types - that is, types that + // require itself in the constructor. + if (types.length < lastCount) { + if (!contains(types, type)) { + minimum = (Constructor) candidate; + lastCount = types.length; + + // Don't loop again if we've already found the best possible constructor + if (lastCount == 0) + break; + } + } + } + + return minimum; + } + /** * Retrieves a default instance or value that is assignable to this type. *

@@ -136,36 +170,18 @@ public class DefaultInstances { if (value != null) return (T) value; } - - Constructor minimum = null; - int lastCount = Integer.MAX_VALUE; - - // Find the constructor with the fewest parameters - for (Constructor candidate : type.getConstructors()) { - Class[] types = candidate.getParameterTypes(); - - // Note that we don't allow recursive types - that is, types that - // require itself in the constructor. - if (types.length < lastCount) { - if (!contains(types, type)) { - minimum = (Constructor) candidate; - lastCount = types.length; - - // Don't loop again if we've already found the best possible constructor - if (lastCount == 0) - break; - } - } - } - + + Constructor minimum = getMinimumConstructor(type); + // Create the type with this constructor using default values. This might fail, though. try { if (minimum != null) { - Object[] params = new Object[lastCount]; + int parameterCount = minimum.getParameterTypes().length; + Object[] params = new Object[parameterCount]; Class[] types = minimum.getParameterTypes(); // Fill out - for (int i = 0; i < lastCount; i++) { + for (int i = 0; i < parameterCount; i++) { params[i] = getDefaultInternal(types[i], providers, recursionLevel + 1); } @@ -180,6 +196,24 @@ public class DefaultInstances { return null; } + /** + * Construct default instances using the CGLIB enhancer object instead. + * @param enhancer - a CGLIB enhancer to use. + * @return A default instance generator that uses the CGLIB enhancer. + */ + public DefaultInstances forEnhancer(Enhancer enhancer) { + final Enhancer ex = enhancer; + + return new DefaultInstances(registered) { + @SuppressWarnings("unchecked") + @Override + protected T createInstance(Class type, Constructor constructor, Class[] types, Object[] params) { + // Use the enhancer instead + return (T) ex.create(types, params); + } + }; + } + /** * Used by the default instance provider to create a class from a given constructor. * The default method uses reflection. diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/instances/ExistingGenerator.java b/ProtocolLib/src/com/comphenix/protocol/reflect/instances/ExistingGenerator.java new file mode 100644 index 00000000..97bad0a6 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/instances/ExistingGenerator.java @@ -0,0 +1,89 @@ +package com.comphenix.protocol.reflect.instances; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; + +/** + * Provides instance constructors using a list of existing values. + *

+ * Only one instance per individual class. + * @author Kristian + */ +public class ExistingGenerator implements InstanceProvider { + + @SuppressWarnings("rawtypes") + private Map existingValues = new HashMap(); + + private ExistingGenerator() { + // Only accessible to the constructors + } + + /** + * Automatically create an instance provider from a objects public and private fields. + *

+ * If two or more fields share the same type, the last declared non-null field will take + * precedent. + * @param object - object to create an instance generator from. + * @return The instance generator. + */ + public static ExistingGenerator fromObjectFields(Object object) { + ExistingGenerator generator = new ExistingGenerator(); + + // Read instances from every field. + for (Field field : FuzzyReflection.fromObject(object, true).getFields()) { + try { + Object value = FieldUtils.readField(field, object, true); + + // Use the type of the field, not the object itself + if (value != null) + generator.addObject(field.getType(), value); + + } catch (Exception e) { + // Yes, swallow it. No, really. + } + } + + return generator; + } + + /** + * Create an instance generator from a pre-defined array of values. + * @param values - values to provide. + * @return An instance provider that uses these values. + */ + public static ExistingGenerator fromObjectArray(Object[] values) { + ExistingGenerator generator = new ExistingGenerator(); + + for (Object value : values) + generator.addObject(value); + + return generator; + } + + private void addObject(Object value) { + if (value == null) + throw new IllegalArgumentException("Value cannot be NULL."); + + existingValues.put(value.getClass(), value); + } + + private void addObject(Class type, Object value) { + existingValues.put(type, value); + } + + + @Override + public Object create(@Nullable Class type) { + + Object value = existingValues.get(type); + + // NULL values indicate that the generator failed + return value; + } +} diff --git a/ProtocolLib/src/plugin.yml b/ProtocolLib/src/plugin.yml index 91fbefd4..f0fd2022 100644 --- a/ProtocolLib/src/plugin.yml +++ b/ProtocolLib/src/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 1.1.0 +version: 1.2.0 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib diff --git a/Readme.md b/Readme.md index 528610c9..c182ce0a 100644 --- a/Readme.md +++ b/Readme.md @@ -129,5 +129,3 @@ types. It's remarkably consistent across different versions. ### Incompatiblity The following plugins (to be expanded) are not compatible with ProtocolLib: - - * TagAPI \ No newline at end of file