Compare commits

...
24 Commits
Author SHA1 Message Date
Kristian S. Stangeland 88da4e3272 Bumping to version 1.3.0 2012-10-05 03:50:28 +02:00
Kristian S. Stangeland 0e76d8ea2b Make the proxy creation even more flexible.
Now we even support Orebfuscator without using Spout.
2012-10-05 03:12:35 +02:00
Kristian S. Stangeland 18ef06ea21 Adding "support" for Spout by proxying it's NetServerHandler.
While it may seem better to use a Spout PacketListener, we can't
prevent other spout listeners from stopping the listener
chain. For instance, Orebfuscator does supports Spout, but does this
by cancelling every chunk packet and sending them to be processed and
sent by Orebfuscator only. This can never be made compatible with
other plugins.

So, we choose to add some overhead and inject our proxy onto Spout. 
Fortunately, Spout injects the proxy using a player listener on LOWEST, 
so we get to override Spout again with our HIGHEST. The proxy method
should be generic enough to handle most proxy types.
2012-10-05 01:41:17 +02:00
Kristian S. Stangeland af3e278e06 Moved all player related injection methods into a separate package.
This should make the code a little bit clearer.
2012-10-04 21:56:39 +02:00
Kristian S. Stangeland eb12808483 Prevent method #3 from creating uneeded proxy objects.
This would happen if the NetServerHandler is already a proxy, and
the injection method is unable to create an instance of the proxy
type. 

It's best to fail instead of polluting (due to constructors with
side-effects) Minecraft with failed proxy objects.
2012-10-04 17:43:46 +02:00
Kristian S. Stangeland f0651f7170 Don't blow up the fallback method just because a plugin isn't
compatible.
2012-10-04 09:01:33 +02:00
Kristian S. Stangeland debf8c4d88 Create the worker ID before it is run.
In addition, we'll prevent reuse of worker objects.
2012-10-04 06:23:27 +02:00
Kristian S. Stangeland af2d692c59 Wait and notify on the correct lock. 2012-10-04 06:13:19 +02:00
Kristian S. Stangeland db8db1fba1 Make it possible to identify the current worker. 2012-10-04 05:14:17 +02:00
Kristian S. Stangeland e729583d74 Make the manual asynchronous worker closable.
Added a good deal of synchronization to deal with closing a 
specific worker. This overhead can be avoided by simply closing 
any given worker.
2012-10-04 03:20:09 +02:00
Kristian S. Stangeland 8a5e5e849b Attempt to work around existing injected proxies.
This didn't work for Orebfuscator though.
2012-10-04 00:58:05 +02:00
Kristian S. Stangeland 558eab2253 Use an atomic integer instead of a volatile field.
Incrementing and decrementing is not thread-safe on volatile variables.
2012-10-03 23:13:05 +02:00
Kristian S. Stangeland c6fb01e1e1 Adding the ability to use multiple worker threads. 2012-10-03 23:10:35 +02:00
Kristian S. Stangeland eb8ac33a3e Removed uncompleted code. 2012-10-02 23:07:03 +02:00
Kristian S. Stangeland 3f1b5d49e9 Ignore offline players when sending overdue packets. 2012-10-02 23:05:31 +02:00
Kristian S. Stangeland 1e1875cbd8 Speed up the proxy object by using a NoOp filter. 2012-10-02 04:36:47 +02:00
Kristian S. Stangeland 48cedd20d4 Add some size methods. 2012-10-02 04:02:54 +02:00
Kristian S. Stangeland e4e4581717 Bumping to 1.2.2 2012-10-02 03:55:07 +02:00
Kristian S. Stangeland 2fceaa803e Fixed the "moved too quickly" error that would cause players to
fall out into the void on 1.3.2.
2012-10-02 03:54:50 +02:00
Kristian S. Stangeland 0470b2335c Bumping to 1.2.1 2012-10-01 21:47:36 +02:00
Kristian S. Stangeland fe55bb2e56 Processed packets is now sorted by the sending index. 2012-10-01 21:47:19 +02:00
Kristian S. Stangeland 7b35dd954c Switching to the network server object. 2012-10-01 20:30:56 +02:00
Kristian S. Stangeland f4f28023fa Always create the NullPacketListener (unless otherwise stated).
This functions like a crude "counter" that automatically unregisters 
the packet injectors when every asynchronous listener has been removed.
2012-10-01 06:04:31 +02:00
Kristian S. Stangeland 961b34da38 Update documentation (may still need some work). 2012-10-01 04:59:27 +02:00
27 changed files with 1720 additions and 529 deletions

No files matched your search

@@ -20,7 +20,6 @@ public interface AsynchronousManager {
* Registers an asynchronous packet handler.
* <p>
* 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.
*/
@@ -53,6 +53,7 @@ public class AsyncFilterManager implements AsynchronousManager {
// Server packets are synchronized already
this.serverQueue = new PacketSendingQueue(false);
// Client packets must be synchronized
this.clientQueue = new PacketSendingQueue(true);
@@ -68,28 +69,40 @@ public class AsyncFilterManager implements AsynchronousManager {
@Override
public AsyncListenerHandler registerAsyncHandler(PacketListener listener) {
return registerAsyncHandler(listener, true);
}
/**
* Registers an asynchronous packet handler.
* <p>
* To start listening asynchronously, pass the getListenerLoop() runnable to a different thread.
* <p>
* Asynchronous events will only be executed if a synchronous listener with the same packets is registered.
* If you already have a synchronous event, call this method with autoInject set to FALSE.
*
* @param listener - the packet listener that will recieve these asynchronous events.
* @param autoInject - whether or not to automatically create the corresponding synchronous listener,
* @return An asynchrouns handler.
*/
public AsyncListenerHandler registerAsyncHandler(PacketListener listener, boolean autoInject) {
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) {
// We need a synchronized listener to get the ball rolling
if (autoInject) {
handler.setNullPacketListener(new NullPacketListener(listener));
manager.addPacketListener(handler.getNullPacketListener());
}
@@ -97,15 +110,6 @@ public class AsyncFilterManager implements AsynchronousManager {
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;
}
@@ -182,7 +186,7 @@ public class AsyncFilterManager implements AsynchronousManager {
// Start the process
getSendingQueue(syncPacket).enqueue(newEvent);
// We know this is occuring on the main thread, to pass TRUE
// We know this is occuring on the main thread, so pass TRUE
getProcessingQueue(syncPacket).enqueue(newEvent, true);
}
@@ -1,6 +1,10 @@
package com.comphenix.protocol.async;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import org.bukkit.plugin.Plugin;
@@ -17,18 +21,26 @@ import com.comphenix.protocol.events.PacketListener;
public class AsyncListenerHandler {
/**
* Signal an end to the packet processing.
* Signal an end to packet processing.
*/
private static final PacketEvent INTERUPT_PACKET = new PacketEvent(new Object());
/**
* Called when the threads have to wake up for something important.
*/
private static final PacketEvent WAKEUP_PACKET = new PacketEvent(new Object());
// Unique worker ID
private static final AtomicInteger nextID = new AtomicInteger();
// 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;
// Number of worker threads
private final AtomicInteger started = new AtomicInteger();
// The packet listener
private PacketListener listener;
@@ -40,6 +52,10 @@ public class AsyncListenerHandler {
// List of queued packets
private ArrayBlockingQueue<PacketEvent> queuedPackets = new ArrayBlockingQueue<PacketEvent>(DEFAULT_CAPACITY);
// List of cancelled tasks
private final Set<Integer> stoppedTasks = new HashSet<Integer>();
private final Object stopLock = new Object();
// Minecraft main thread
private Thread mainThread;
@@ -78,18 +94,26 @@ public class AsyncListenerHandler {
return nullPacketListener;
}
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;
}
/**
* 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.
@@ -103,46 +127,208 @@ public class AsyncListenerHandler {
}
/**
* Create a runnable that will initiate the listener loop.
* Create a worker that will initiate the listener loop. Note that using stop() to
* close a specific worker is less efficient than stopping an arbitrary worker.
* <p>
* <b>Warning</b>: Never call the run() method in the main thread.
*/
public Runnable getListenerLoop() {
return new Runnable() {
public AsyncRunnable getListenerLoop() {
return new AsyncRunnable() {
private final AtomicBoolean firstRun = new AtomicBoolean();
private final AtomicBoolean finished = new AtomicBoolean();
private final int id = nextID.incrementAndGet();
@Override
public int getID() {
return id;
}
@Override
public void run() {
listenerLoop();
// Careful now
if (firstRun.compareAndSet(false, true)) {
listenerLoop(id);
synchronized (stopLock) {
stoppedTasks.remove(id);
stopLock.notifyAll();
finished.set(true);
}
} else {
if (finished.get())
throw new IllegalStateException(
"This listener has already been run. Create a new instead.");
else
throw new IllegalStateException(
"This listener loop has already been started. Create a new instead.");
}
}
@Override
public boolean stop() throws InterruptedException {
synchronized (stopLock) {
if (!isRunning())
return false;
stoppedTasks.add(id);
// Wake up threads - we have a listener to stop
for (int i = 0; i < getWorkers(); i++) {
queuedPackets.offer(WAKEUP_PACKET);
}
finished.set(true);
waitForStops();
return true;
}
}
@Override
public boolean isRunning() {
return firstRun.get() && !finished.get();
}
@Override
public boolean isFinished() {
return finished.get();
}
};
}
/**
* Start a singler worker thread handling the asynchronous.
*/
public synchronized void start() {
if (listener.getPlugin() == null)
throw new IllegalArgumentException("Cannot start task without a valid plugin.");
if (cancelled)
throw new IllegalStateException("Cannot start a worker when the listener is closing.");
filterManager.scheduleAsyncTask(listener.getPlugin(), getListenerLoop());
}
/**
* Start multiple worker threads for this listener.
* @param count - number of worker threads to start.
*/
public synchronized void start(int count) {
for (int i = 0; i < count; i++)
start();
}
/**
* Stop a worker thread.
*/
public synchronized void stop() {
queuedPackets.add(INTERUPT_PACKET);
}
/**
* Stop the given amount of worker threads.
* @param count - number of threads to stop.
*/
public synchronized void stop(int count) {
for (int i = 0; i < count; i++)
stop();
}
/**
* Set the current number of workers.
* <p>
* This method can only be called with a count of zero when the listener is closing.
* @param count - new number of workers.
*/
public synchronized void setWorkers(int count) {
if (count < 0)
throw new IllegalArgumentException("Number of workers cannot be less than zero.");
if (count > DEFAULT_CAPACITY)
throw new IllegalArgumentException("Cannot initiate more than " + DEFAULT_CAPACITY + " workers");
if (cancelled && count > 0)
throw new IllegalArgumentException("Cannot add workers when the listener is closing.");
long time = System.currentTimeMillis();
// Try to get to the correct count
while (started.get() != count) {
if (started.get() < count)
start();
else
stop();
// May happen if another thread is doing something similar to "setWorkers"
if ((System.currentTimeMillis() - time) > 1000)
throw new RuntimeException("Failed to set worker count.");
}
}
/**
* Retrieve the current number of registered workers.
* <p>
* Note that the returned value may be out of data.
* @return Number of registered workers.
*/
public synchronized int getWorkers() {
return started.get();
}
/**
* Wait until every tasks scheduled to stop has actually stopped.
* @return TRUE if the current listener should stop, FALSE otherwise.
* @throws InterruptedException - If the current thread was interrupted.
*/
private boolean waitForStops() throws InterruptedException {
synchronized (stopLock) {
while (stoppedTasks.size() > 0 && !cancelled) {
stopLock.wait();
}
return cancelled;
}
}
// DO NOT call this method from the main thread
private void listenerLoop() {
private void listenerLoop(int workerID) {
// 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 {
// Wait if certain threads are stopping
if (waitForStops())
return;
// Proceed
started.incrementAndGet();
mainLoop:
while (!cancelled) {
PacketEvent packet = queuedPackets.take();
AsyncMarker marker = packet.getAsyncMarker();
// Handle cancel requests
if (packet == null || marker == null || !packet.isAsynchronous()) {
break;
if (packet == null || marker == null || packet == INTERUPT_PACKET) {
return;
} else if (packet == WAKEUP_PACKET) {
// This is a bit slow, but it should be safe
synchronized (stopLock) {
// Are we the one who is supposed to stop?
if (stoppedTasks.contains(workerID))
return;
if (waitForStops())
return;
}
}
// Here's the core of the asynchronous processing
try {
marker.setListenerHandler(this);
marker.setWorkerID(workerID);
if (packet.isServerPacket())
listener.onPacketSending(packet);
else
@@ -171,40 +357,35 @@ public class AsyncListenerHandler {
} catch (InterruptedException e) {
// We're done
} finally {
// Clean up
started.decrementAndGet();
close();
}
// Clean up
close();
}
private void close() {
private synchronized void close() {
// Remove the listener itself
if (!cancelled) {
filterManager.unregisterAsyncHandlerInternal(this);
cancelled = true;
started = false;
// Tell every uncancelled thread to end
stopThreads();
}
}
private String getPluginName() {
return PacketAdapter.getPluginName(listener);
}
/**
* Retrieve the plugin associated with this async listener.
* @return The plugin.
* Use the poision pill method to stop every worker thread.
*/
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.");
private void stopThreads() {
// Poison Pill Shutdown
queuedPackets.clear();
stop(started.get());
filterManager.scheduleAsyncTask(listener.getPlugin(), getListenerLoop());
// Individual shut down is irrelevant now
synchronized (stopLock) {
stopLock.notifyAll();
}
}
}
@@ -18,6 +18,8 @@ import com.google.common.primitives.Longs;
/**
* Contains information about the packet that is being processed by asynchronous listeners.
* <p>
* Asynchronous listeners can use this to set packet timeout or transmission order.
*
* @author Kristian
*/
@@ -65,6 +67,10 @@ public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
// Whether or not the asynchronous processing itself should be cancelled
private volatile boolean asyncCancelled;
// Used to identify the asynchronous worker
private AsyncListenerHandler listenerHandler;
private int workerID;
// Determine if Minecraft processes this packet asynchronously
private static Method isMinecraftAsync;
private static boolean alwaysSync;
@@ -212,12 +218,48 @@ public class AsyncMarker implements Serializable, Comparable<AsyncMarker> {
public void setAsyncCancelled(boolean asyncCancelled) {
this.asyncCancelled = asyncCancelled;
}
/**
* Retrieve the current asynchronous listener handler.
* @return Asychronous listener handler, or NULL if this packet is not asynchronous.
*/
public AsyncListenerHandler getListenerHandler() {
return listenerHandler;
}
/**
* Set the current asynchronous listener handler.
* <p>
* Used by the worker to update the value.
* @param listenerHandler - new listener handler.
*/
void setListenerHandler(AsyncListenerHandler listenerHandler) {
this.listenerHandler = listenerHandler;
}
/**
* Retrieve the current worker ID.
* @return Current worker ID.
*/
public int getWorkerID() {
return workerID;
}
/**
* Set the current worker ID.
* <p>
* Used by the worker.
* @param workerID - new worker ID.
*/
void setWorkerID(int workerID) {
this.workerID = workerID;
}
/**
* Retrieve iterator for the next listener in line.
* @return Next async packet listener iterator.
*/
public Iterator<PrioritizedListener<AsyncListenerHandler>> getListenerTraversal() {
Iterator<PrioritizedListener<AsyncListenerHandler>> getListenerTraversal() {
return listenerTraversal;
}
@@ -0,0 +1,35 @@
package com.comphenix.protocol.async;
/**
* A runnable representing a asynchronous event listener.
*
* @author Kristian
*/
public interface AsyncRunnable extends Runnable {
/**
* Retrieve a unique worker ID.
* @return Unique worker ID.
*/
public int getID();
/**
* Stop the given runnable.
* <p>
* This may not occur right away.
* @return TRUE if the thread was stopped, FALSE if it was already stopped.
*/
public boolean stop() throws InterruptedException;
/**
* Determine if we're running or not.
* @return TRUE if we're running, FALSE otherwise.
*/
public boolean isRunning();
/**
* Determine if this runnable has already run its course.
* @return TRUE if it has been stopped, FALSE otherwise.
*/
boolean isFinished();
}
@@ -0,0 +1,40 @@
package com.comphenix.protocol.async;
import com.comphenix.protocol.events.PacketEvent;
import com.google.common.base.Preconditions;
import com.google.common.collect.ComparisonChain;
/**
* Provides a comparable to a packet event.
*
* @author Kristian
*/
class PacketEventHolder implements Comparable<PacketEventHolder> {
private PacketEvent event;
/**
* A wrapper that ensures the packet event is ordered by sending index.
* @param event - packet event to wrap.
*/
public PacketEventHolder(PacketEvent event) {
this.event = Preconditions.checkNotNull(event, "Event must be non-null");
}
/**
* Retrieve the stored event.
* @return The stored event.
*/
public PacketEvent getEvent() {
return event;
}
@Override
public int compareTo(PacketEventHolder other) {
AsyncMarker marker = other != null ? other.getEvent().getAsyncMarker() : null;
return ComparisonChain.start().
compare(event.getAsyncMarker(), marker).
result();
}
}
@@ -2,12 +2,14 @@ package com.comphenix.protocol.async;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.Queue;
import java.util.concurrent.Semaphore;
import com.comphenix.protocol.concurrency.AbstractConcurrentListenerMultimap;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.PrioritizedListener;
import com.google.common.collect.MinMaxPriorityQueue;
/**
* Handles the processing of every packet type.
@@ -16,6 +18,9 @@ import com.comphenix.protocol.injector.PrioritizedListener;
*/
class PacketProcessingQueue extends AbstractConcurrentListenerMultimap<AsyncListenerHandler> {
// Initial number of elements
public static final int INITIAL_CAPACITY = 64;
/**
* Default maximum number of packets to process concurrently.
*/
@@ -33,18 +38,23 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap<AsyncList
private Semaphore concurrentProcessing;
// Queued packets for being processed
private ArrayBlockingQueue<PacketEvent> processingQueue;
private Queue<PacketEventHolder> processingQueue;
// Packets for sending
private PacketSendingQueue sendingQueue;
public PacketProcessingQueue(PacketSendingQueue sendingQueue) {
this(sendingQueue, DEFAULT_QUEUE_LIMIT, DEFAULT_MAXIMUM_CONCURRENCY);
this(sendingQueue, INITIAL_CAPACITY, DEFAULT_QUEUE_LIMIT, DEFAULT_MAXIMUM_CONCURRENCY);
}
public PacketProcessingQueue(PacketSendingQueue sendingQueue, int queueLimit, int maximumConcurrency) {
public PacketProcessingQueue(PacketSendingQueue sendingQueue, int initialSize, int maximumSize, int maximumConcurrency) {
super();
this.processingQueue = new ArrayBlockingQueue<PacketEvent>(queueLimit);
this.processingQueue = Synchronization.queue(MinMaxPriorityQueue.
expectedSize(initialSize).
maximumSize(maximumSize).
<PacketEventHolder>create(), null);
this.maximumConcurrency = maximumConcurrency;
this.concurrentProcessing = new Semaphore(maximumConcurrency);
this.sendingQueue = sendingQueue;
@@ -58,8 +68,8 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap<AsyncList
*/
public boolean enqueue(PacketEvent packet, boolean onMainThread) {
try {
processingQueue.add(packet);
processingQueue.add(new PacketEventHolder(packet));
// Begin processing packets
signalBeginProcessing(onMainThread);
return true;
@@ -68,18 +78,27 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimap<AsyncList
}
}
/**
* Number of packet events in the queue.
* @return The number of packet events in the queue.
*/
public int size() {
return processingQueue.size();
}
/**
* 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();
PacketEventHolder holder = processingQueue.poll();
// Any packet queued?
if (packet != null) {
Collection<PrioritizedListener<AsyncListenerHandler>> list = getListener(packet.getPacketID());
if (holder != null) {
PacketEvent packet = holder.getEvent();
AsyncMarker marker = packet.getAsyncMarker();
Collection<PrioritizedListener<AsyncListenerHandler>> list = getListener(packet.getPacketID());
// Yes, removing the marker will cause the chain to stop
if (list != null) {
@@ -1,15 +1,15 @@
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 org.bukkit.entity.Player;
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.
@@ -17,28 +17,28 @@ import com.google.common.collect.ComparisonChain;
*/
class PacketSendingQueue {
private static final int INITIAL_CAPACITY = 64;
public static final int INITIAL_CAPACITY = 64;
private PriorityBlockingQueue<PacketEvent> sendingQueue;
private PriorityBlockingQueue<PacketEventHolder> sendingQueue;
// Whether or not packet transmission can only occur on the main thread
private final boolean synchronizeMain;
/**
* Number of packet events in the queue.
* @return The number of packet events in the queue.
*/
public int size() {
return sendingQueue.size();
}
/**
* Create a packet sending queue.
* @param synchronizeMain - whether or not to synchronize with the main thread.
*/
public PacketSendingQueue(boolean synchronizeMain) {
this.sendingQueue = new PriorityBlockingQueue<PacketEventHolder>(INITIAL_CAPACITY);
this.synchronizeMain = synchronizeMain;
this.sendingQueue = new PriorityBlockingQueue<PacketEvent>(INITIAL_CAPACITY, new Comparator<PacketEvent>() {
// Compare using the async marker
@Override
public int compare(PacketEvent o1, PacketEvent o2) {
return ComparisonChain.start().
compare(o1.getAsyncMarker(), o2.getAsyncMarker()).
result();
}
});
}
/**
@@ -46,7 +46,7 @@ class PacketSendingQueue {
* @param packet
*/
public void enqueue(PacketEvent packet) {
sendingQueue.add(packet);
sendingQueue.add(new PacketEventHolder(packet));
}
/**
@@ -70,7 +70,9 @@ class PacketSendingQueue {
Set<Integer> lookup = new HashSet<Integer>(packetsRemoved);
// Note that this is O(n), so it might be expensive
for (PacketEvent event : sendingQueue) {
for (PacketEventHolder holder : sendingQueue) {
PacketEvent event = holder.getEvent();
if (lookup.contains(event.getPacketID())) {
event.getAsyncMarker().setProcessed(true);
}
@@ -88,9 +90,10 @@ class PacketSendingQueue {
// Transmit as many packets as we can
while (true) {
PacketEvent current = sendingQueue.peek();
PacketEventHolder holder = sendingQueue.peek();
if (current != null) {
if (holder != null) {
PacketEvent current = holder.getEvent();
AsyncMarker marker = current.getAsyncMarker();
// Abort if we're not on the main thread
@@ -111,7 +114,10 @@ class PacketSendingQueue {
if (marker.isProcessed() || marker.hasExpired()) {
if (marker.isProcessed() && !current.isCancelled()) {
sendPacket(current);
// Silently skip players that have logged out
if (isOnline(current.getPlayer())) {
sendPacket(current);
}
}
sendingQueue.poll();
@@ -124,15 +130,19 @@ class PacketSendingQueue {
}
}
private boolean isOnline(Player player) {
return player != null && player.isOnline();
}
/**
* Send every packet, regardless of the processing state.
*/
private void forceSend() {
while (true) {
PacketEvent current = sendingQueue.poll();
PacketEventHolder holder = sendingQueue.poll();
if (current != null) {
sendPacket(current);
if (holder != null) {
sendPacket(holder.getEvent());
} else {
break;
}
@@ -0,0 +1,211 @@
package com.comphenix.protocol.async;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Queue;
import javax.annotation.Nullable;
import com.google.common.base.Preconditions;
/**
* Synchronization views copied from Google Guava.
*
* @author Kristian
*/
class Synchronization {
/**
* Create a synchronized wrapper for the given queue.
* <p>
* This wrapper cannot synchronize the iterator(). Callers are expected
* to synchronize iterators manually.
* @param queue - the queue to synchronize.
* @param mutex - synchronization mutex, or NULL to use the queue.
* @return A synchronization wrapper.
*/
public static <E> Queue<E> queue(Queue<E> queue, @Nullable Object mutex) {
return (queue instanceof SynchronizedQueue) ?
queue :
new SynchronizedQueue<E>(queue, mutex);
}
private static class SynchronizedObject implements Serializable {
private static final long serialVersionUID = -4408866092364554628L;
final Object delegate;
final Object mutex;
SynchronizedObject(Object delegate, @Nullable Object mutex) {
this.delegate = Preconditions.checkNotNull(delegate);
this.mutex = (mutex == null) ? this : mutex;
}
Object delegate() {
return delegate;
}
// No equals and hashCode; see ForwardingObject for details.
@Override
public String toString() {
synchronized (mutex) {
return delegate.toString();
}
}
}
private static class SynchronizedCollection<E> extends SynchronizedObject implements Collection<E> {
private static final long serialVersionUID = 5440572373531285692L;
private SynchronizedCollection(Collection<E> delegate,
@Nullable Object mutex) {
super(delegate, mutex);
}
@SuppressWarnings("unchecked")
@Override
Collection<E> delegate() {
return (Collection<E>) super.delegate();
}
@Override
public boolean add(E e) {
synchronized (mutex) {
return delegate().add(e);
}
}
@Override
public boolean addAll(Collection<? extends E> c) {
synchronized (mutex) {
return delegate().addAll(c);
}
}
@Override
public void clear() {
synchronized (mutex) {
delegate().clear();
}
}
@Override
public boolean contains(Object o) {
synchronized (mutex) {
return delegate().contains(o);
}
}
@Override
public boolean containsAll(Collection<?> c) {
synchronized (mutex) {
return delegate().containsAll(c);
}
}
@Override
public boolean isEmpty() {
synchronized (mutex) {
return delegate().isEmpty();
}
}
@Override
public Iterator<E> iterator() {
return delegate().iterator(); // manually synchronized
}
@Override
public boolean remove(Object o) {
synchronized (mutex) {
return delegate().remove(o);
}
}
@Override
public boolean removeAll(Collection<?> c) {
synchronized (mutex) {
return delegate().removeAll(c);
}
}
@Override
public boolean retainAll(Collection<?> c) {
synchronized (mutex) {
return delegate().retainAll(c);
}
}
@Override
public int size() {
synchronized (mutex) {
return delegate().size();
}
}
@Override
public Object[] toArray() {
synchronized (mutex) {
return delegate().toArray();
}
}
@Override
public <T> T[] toArray(T[] a) {
synchronized (mutex) {
return delegate().toArray(a);
}
}
}
private static class SynchronizedQueue<E> extends SynchronizedCollection<E> implements Queue<E> {
private static final long serialVersionUID = 1961791630386791902L;
SynchronizedQueue(Queue<E> delegate, @Nullable Object mutex) {
super(delegate, mutex);
}
@Override
Queue<E> delegate() {
return (Queue<E>) super.delegate();
}
@Override
public E element() {
synchronized (mutex) {
return delegate().element();
}
}
@Override
public boolean offer(E e) {
synchronized (mutex) {
return delegate().offer(e);
}
}
@Override
public E peek() {
synchronized (mutex) {
return delegate().peek();
}
}
@Override
public E poll() {
synchronized (mutex) {
return delegate().poll();
}
}
@Override
public E remove() {
synchronized (mutex) {
return delegate().remove();
}
}
}
}
@@ -0,0 +1,27 @@
package com.comphenix.protocol.injector;
import net.minecraft.server.Packet;
import com.comphenix.protocol.events.PacketEvent;
public interface ListenerInvoker {
/**
* Invokes the given packet event for every registered listener.
* @param event - the packet event to invoke.
*/
public abstract void invokePacketRecieving(PacketEvent event);
/**
* Invokes the given packet event for every registered listener.
* @param event - the packet event to invoke.
*/
public abstract void invokePacketSending(PacketEvent event);
/**
* Retrieve the associated ID of a packet.
* @param packet - the packet.
* @return The packet ID.
*/
public abstract int getPacketID(Packet packet);
}
@@ -1,181 +0,0 @@
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<Object> serverHandlerModifier;
private InjectedServerConnection serverInjection;
public NetworkServerInjector(Player player, PacketFilterManager manager,
Set<Integer> 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<Object>(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<Object> modifierSource = serverHandlerModifier.withTarget(source);
StructureModifier<Object> 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
}
}
@@ -17,13 +17,10 @@
package com.comphenix.protocol.injector;
import java.io.DataInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
@@ -51,12 +48,13 @@ 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.injector.player.PlayerInjectionHandler;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
public final class PacketFilterManager implements ProtocolManager {
public final class PacketFilterManager implements ProtocolManager, ListenerInvoker {
/**
* Sets the inject hook type. Different types allow for maximum compatibility.
@@ -86,22 +84,12 @@ public final class PacketFilterManager implements ProtocolManager {
// Create a concurrent set
private Set<PacketListener> packetListeners =
Collections.newSetFromMap(new ConcurrentHashMap<PacketListener, Boolean>());
// Player injection
private Map<DataInputStream, Player> connectionLookup = new ConcurrentHashMap<DataInputStream, Player>();
private Map<Player, PlayerInjector> playerInjection = new HashMap<Player, PlayerInjector>();
// Player injection type
private PlayerInjectHooks playerHook = PlayerInjectHooks.NETWORK_HANDLER_FIELDS;
// Packet injection
private PacketInjector packetInjector;
// Server connection injection
private InjectedServerConnection serverInjection;
// Enabled packet filters
private Set<Integer> sendingFilters = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
// Player injection
private PlayerInjectionHandler playerInjection;
// The two listener containers
private SortedPacketListenerList recievedListeners = new SortedPacketListenerList();
@@ -112,10 +100,7 @@ public final class PacketFilterManager implements ProtocolManager {
// The default class loader
private ClassLoader classLoader;
// The last successful player hook
private PlayerInjector lastSuccessfulHook;
// Error logger
private Logger logger;
@@ -135,9 +120,9 @@ public final class PacketFilterManager implements ProtocolManager {
// Initialize values
this.classLoader = classLoader;
this.logger = logger;
this.packetInjector = new PacketInjector(classLoader, this, connectionLookup);
this.playerInjection = new PlayerInjectionHandler(classLoader, logger, this, server);
this.packetInjector = new PacketInjector(classLoader, this, playerInjection);
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);
}
@@ -153,7 +138,7 @@ public final class PacketFilterManager implements ProtocolManager {
* @return Injection method for reading server packets.
*/
public PlayerInjectHooks getPlayerHook() {
return playerHook;
return playerInjection.getPlayerHook();
}
/**
@@ -161,14 +146,10 @@ public final class PacketFilterManager implements ProtocolManager {
* @param playerHook - the new injection method for reading server packets.
*/
public void setPlayerHook(PlayerInjectHooks playerHook) {
this.playerHook = playerHook;
playerInjection.setPlayerHook(playerHook);
// Make sure the current listeners are compatible
if (lastSuccessfulHook != null) {
for (PacketListener listener : packetListeners) {
checkListener(listener);
}
}
playerInjection.checkListener(packetListeners);
}
public Logger getLogger() {
@@ -200,14 +181,14 @@ public final class PacketFilterManager implements ProtocolManager {
verifyWhitelist(listener, sending);
sendingListeners.addListener(listener, sending);
enablePacketFilters(ConnectionSide.SERVER_SIDE, sending.getWhitelist());
// Make sure this is possible
playerInjection.checkListener(listener);
}
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
@@ -230,21 +211,7 @@ public final class PacketFilterManager implements ProtocolManager {
}
}
}
/**
* 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)
@@ -288,18 +255,12 @@ public final class PacketFilterManager implements ProtocolManager {
asyncFilterManager.unregisterAsyncHandlers(plugin);
}
/**
* Invokes the given packet event for every registered listener.
* @param event - the packet event to invoke.
*/
@Override
public void invokePacketRecieving(PacketEvent event) {
handlePacket(recievedListeners, event, false);
}
/**
* Invokes the given packet event for every registered listener.
* @param event - the packet event to invoke.
*/
@Override
public void invokePacketSending(PacketEvent event) {
handlePacket(sendingListeners, event, true);
}
@@ -352,8 +313,8 @@ public final class PacketFilterManager implements ProtocolManager {
for (int packetID : packets) {
if (side.isForServer())
sendingFilters.add(packetID);
if (side.isForClient() && packetInjector != null)
playerInjection.addPacketHandler(packetID);
if (side.isForClient() && packetInjector != null)
packetInjector.addPacketHandler(packetID);
}
}
@@ -369,7 +330,7 @@ public final class PacketFilterManager implements ProtocolManager {
for (int packetID : packets) {
if (side.isForServer())
sendingFilters.remove(packetID);
playerInjection.removePacketHandler(packetID);
if (side.isForClient() && packetInjector != null)
packetInjector.removePacketHandler(packetID);
}
@@ -387,7 +348,7 @@ public final class PacketFilterManager implements ProtocolManager {
if (packet == null)
throw new IllegalArgumentException("packet cannot be NULL.");
getInjector(reciever).sendServerPacket(packet.getHandle(), filters);
playerInjection.sendServerPacket(reciever, packet, filters);
}
@Override
@@ -403,17 +364,21 @@ public final class PacketFilterManager implements ProtocolManager {
if (packet == null)
throw new IllegalArgumentException("packet cannot be NULL.");
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);
PacketEvent event = packetInjector.packetRecieved(packet, sender);
if (!event.isCancelled())
mcPacket = event.getPacket().getHandle();
else
return;
}
injector.processPacket(mcPacket);
playerInjection.processPacket(sender, mcPacket);
}
@Override
@@ -444,7 +409,7 @@ public final class PacketFilterManager implements ProtocolManager {
@Override
public Set<Integer> getSendingFilters() {
return ImmutableSet.copyOf(sendingFilters);
return playerInjection.getSendingFilters();
}
@Override
@@ -463,83 +428,9 @@ public final class PacketFilterManager implements ProtocolManager {
*/
public void initializePlayers(Player[] players) {
for (Player player : players)
injectPlayer(player);
playerInjection.injectPlayer(player);
}
/**
* 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 getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException {
// Construct the correct player hook
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.");
}
}
/**
* Initialize a player hook, allowing us to read server packets.
* @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)) {
while (true) {
try {
injector = getHookInstance(player, currentHook);
injector.injectManager();
playerInjection.put(player, injector);
connectionLookup.put(injector.getInputStream(false), player);
break;
} catch (Exception 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);
}
}
/**
* Register this protocol manager on Bukkit.
* @param manager - Bukkit plugin manager that provides player join/leave events.
@@ -550,17 +441,17 @@ public final class PacketFilterManager implements ProtocolManager {
try {
manager.registerEvents(new Listener() {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
injectPlayer(event.getPlayer());
playerInjection.injectPlayer(event.getPlayer());
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
uninjectPlayer(event.getPlayer());
playerInjection.uninjectPlayer(event.getPlayer());
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPluginDisabled(PluginDisableEvent event) {
// Clean up in case the plugin forgets
if (event.getPlugin() != plugin) {
@@ -576,6 +467,14 @@ public final class PacketFilterManager implements ProtocolManager {
}
}
@Override
public int getPacketID(Packet packet) {
if (packet == null)
throw new IllegalArgumentException("Packet cannot be NULL.");
return MinecraftRegistry.getPacketToID().get(packet.getClass());
}
// Yes, this is crazy.
@SuppressWarnings({ "unchecked", "rawtypes" })
private void registerOld(PluginManager manager, Plugin plugin) {
@@ -588,7 +487,7 @@ public final class PacketFilterManager implements ProtocolManager {
Class eventPriority = loader.loadClass("org.bukkit.event.Event$Priority");
// Get the priority
Object priorityNormal = Enum.valueOf(eventPriority, "Normal");
Object priorityNormal = Enum.valueOf(eventPriority, "Highest");
// Get event types
Object playerJoinType = Enum.valueOf(eventTypes, "PLAYER_JOIN");
@@ -617,9 +516,9 @@ public final class PacketFilterManager implements ProtocolManager {
// Check for the correct event
if (event instanceof PlayerJoinEvent)
injectPlayer(((PlayerJoinEvent) event).getPlayer());
playerInjection.injectPlayer(((PlayerJoinEvent) event).getPlayer());
else if (event instanceof PlayerQuitEvent)
uninjectPlayer(((PlayerQuitEvent) event).getPlayer());
playerInjection.uninjectPlayer(((PlayerQuitEvent) event).getPlayer());
}
return null;
}
@@ -661,37 +560,7 @@ public final class PacketFilterManager implements ProtocolManager {
e.printStackTrace();
}
}
private void uninjectPlayer(Player player) {
if (!hasClosed && player != null) {
PlayerInjector injector = playerInjection.get(player);
if (injector != null) {
DataInputStream input = injector.getInputStream(true);
injector.cleanupAll();
playerInjection.remove(player);
connectionLookup.remove(input);
}
}
}
private PlayerInjector getInjector(Player player) {
if (!playerInjection.containsKey(player)) {
// What? Try to inject again.
injectPlayer(player);
}
PlayerInjector injector = playerInjection.get(player);
// Check that the injector was sucessfully added
if (injector != null)
return injector;
else
throw new IllegalArgumentException("Player has no injected handler.");
}
/**
* Retrieves the current plugin class loader.
* @return Class loader.
@@ -707,28 +576,19 @@ public final class PacketFilterManager implements ProtocolManager {
public void close() {
// Guard
if (hasClosed || playerInjection == null)
if (hasClosed)
return;
// Remove everything
for (PlayerInjector injection : playerInjection.values()) {
if (injection != null) {
injection.cleanupAll();
}
}
// Remove packet handlers
if (packetInjector != null)
packetInjector.cleanupAll();
// Remove server handler
serverInjection.cleanupAll();
playerInjection.close();
hasClosed = true;
// Remove listeners
packetListeners.clear();
playerInjection.clear();
connectionLookup.clear();
// Clean up async handlers. We have to do this last.
asyncFilterManager.cleanupAll();
@@ -33,6 +33,7 @@ import net.sf.cglib.proxy.Enhancer;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
@@ -48,10 +49,10 @@ class PacketInjector {
private static Object intHashMap;
// The packet filter manager
private PacketFilterManager manager;
private ListenerInvoker manager;
// Allows us to determine the sender
private Map<DataInputStream, Player> playerLookup;
private PlayerInjectionHandler playerInjection;
// Allows us to look up read packet injectors
private Map<Integer, ReadPacketModifier> readModifier;
@@ -59,12 +60,12 @@ class PacketInjector {
// Class loader
private ClassLoader classLoader;
public PacketInjector(ClassLoader classLoader, PacketFilterManager manager,
Map<DataInputStream, Player> playerLookup) throws IllegalAccessException {
public PacketInjector(ClassLoader classLoader, ListenerInvoker manager,
PlayerInjectionHandler playerInjection) throws IllegalAccessException {
this.classLoader = classLoader;
this.manager = manager;
this.playerLookup = playerLookup;
this.playerInjection = playerInjection;
this.readModifier = new ConcurrentHashMap<Integer, ReadPacketModifier>();
initialize();
}
@@ -194,7 +195,18 @@ class PacketInjector {
// Called from the ReadPacketModified monitor
PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) {
Player client = playerLookup.get(input);
Player client = playerInjection.getPlayerByConnection(input);
return packetRecieved(packet, client);
}
/**
* Let the packet listeners process the given packet.
* @param packet - a packet to process.
* @param client - the client that sent the packet.
* @return The resulting packet event.
*/
public PacketEvent packetRecieved(PacketContainer packet, Player client) {
PacketEvent event = PacketEvent.fromClient((Object) manager, packet, client);
manager.invokePacketRecieving(event);
@@ -0,0 +1,41 @@
package com.comphenix.protocol.injector;
/**
* Invoked when attempting to use a player that has already logged out.
*
* @author Kristian
*/
public class PlayerLoggedOutException extends RuntimeException {
/**
* Generated by Eclipse.
*/
private static final long serialVersionUID = 4889257862160145234L;
public PlayerLoggedOutException() {
// Default error message
super("Cannot inject a player that has already logged out.");
}
public PlayerLoggedOutException(String message, Throwable cause) {
super(message, cause);
}
public PlayerLoggedOutException(String message) {
super(message);
}
public PlayerLoggedOutException(Throwable cause) {
super(cause);
}
/**
* Construct an exception from a formatted message.
* @param message - the message to format.
* @param params - parameters.
* @return The formated exception
*/
public static PlayerLoggedOutException fromFormat(String message, Object... params) {
return new PlayerLoggedOutException(String.format(message, params));
}
}
@@ -1,17 +1,17 @@
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Set;
import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket;
import net.minecraft.server.Packet;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import com.comphenix.protocol.injector.NetworkFieldInjector.FakePacket;
/**
* The array list that notifies when packets are sent by the server.
*
@@ -1,4 +1,4 @@
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -7,10 +7,13 @@ import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.cglib.proxy.Factory;
import org.bukkit.Server;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.ObjectCloner;
import com.comphenix.protocol.reflect.VolatileField;
/**
@@ -134,18 +137,33 @@ class InjectedServerConnection {
if (list instanceof ReplacedArrayList) {
replacedLists.add((ReplacedArrayList<Object>) list);
} else {
replacedLists.add(new ReplacedArrayList<Object>(list));
replacedLists.add(createReplacement(list));
listFieldRef.setValue(replacedLists.get(0));
listFields.add(listFieldRef);
}
}
// Hack to avoid the "moved to quickly" error
private ReplacedArrayList<Object> createReplacement(List<Object> list) {
return new ReplacedArrayList<Object>(list) {
@Override
protected void onReplacing(Object inserting, Object replacement) {
// Is this a normal Minecraft object?
if (!(inserting instanceof Factory)) {
// If so, copy the content of the old element to the new
ObjectCloner.copyTo(inserting, replacement, inserting.getClass());
}
}
};
}
/**
* 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();
}
@@ -1,4 +1,4 @@
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -7,12 +7,14 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
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.injector.ListenerInvoker;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
@@ -45,13 +47,28 @@ class NetworkFieldInjector extends PlayerInjector {
// Sync field
private static Field syncField;
private Object syncObject;
// Determine if we're listening
private Set<Integer> sendingFilters;
// Used to construct proxy objects
private ClassLoader classLoader;
public NetworkFieldInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
super(player, manager, sendingFilters);
public NetworkFieldInjector(ClassLoader classLoader, Logger logger, Player player,
ListenerInvoker manager, Set<Integer> sendingFilters) throws IllegalAccessException {
super(logger, player, manager);
this.classLoader = classLoader;
this.sendingFilters = sendingFilters;
}
@Override
protected synchronized void initialize() throws IllegalAccessException {
protected boolean hasListener(int packetID) {
return sendingFilters.contains(packetID);
}
@Override
public synchronized void initialize() throws IllegalAccessException {
super.initialize();
// Get the sync field as well
@@ -112,7 +129,7 @@ class NetworkFieldInjector extends PlayerInjector {
synchronized(syncObject) {
// The list we'll be inserting
List<Packet> hackedList = new InjectedArrayList(manager.getClassLoader(), this, ignoredPackets);
List<Packet> hackedList = new InjectedArrayList(classLoader, this, ignoredPackets);
// Add every previously stored packet
for (Packet packet : minecraftList) {
@@ -154,4 +171,9 @@ class NetworkFieldInjector extends PlayerInjector {
}
overridenLists.clear();
}
@Override
public boolean canInject() {
return true;
}
}
@@ -1,4 +1,4 @@
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.lang.reflect.InvocationTargetException;
@@ -8,12 +8,14 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.logging.Logger;
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.injector.ListenerInvoker;
/**
* Injection method that overrides the NetworkHandler itself, and it's sendPacket-method.
@@ -21,10 +23,19 @@ import com.comphenix.protocol.events.PacketListener;
* @author Kristian
*/
class NetworkObjectInjector extends PlayerInjector {
public NetworkObjectInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
super(player, manager, sendingFilters);
// Determine if we're listening
private Set<Integer> sendingFilters;
public NetworkObjectInjector(Logger logger, Player player, ListenerInvoker invoker, Set<Integer> sendingFilters) throws IllegalAccessException {
super(logger, player, invoker);
this.sendingFilters = sendingFilters;
}
@Override
protected boolean hasListener(int packetID) {
return sendingFilters.contains(packetID);
}
@Override
public void sendServerPacket(Packet packet, boolean filtered) throws InvocationTargetException {
Object networkDelegate = filtered ? networkManagerRef.getValue() : networkManagerRef.getOldValue();
@@ -107,4 +118,9 @@ class NetworkObjectInjector extends PlayerInjector {
// Clean up
networkManagerRef.revertValue();
}
@Override
public boolean canInject() {
return true;
}
}
@@ -0,0 +1,249 @@
package com.comphenix.protocol.injector.player;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.logging.Logger;
import net.minecraft.server.Packet;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
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 net.sf.cglib.proxy.NoOp;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.ListenerInvoker;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.ObjectCloner;
import com.comphenix.protocol.reflect.VolatileField;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
import com.comphenix.protocol.reflect.instances.ExistingGenerator;
/**
* Represents a player hook into the NetServerHandler class.
*
* @author Kristian
*/
public class NetworkServerInjector extends PlayerInjector {
private static Method sendPacketMethod;
private InjectedServerConnection serverInjection;
// Determine if we're listening
private Set<Integer> sendingFilters;
// Used to create proxy objects
private ClassLoader classLoader;
public NetworkServerInjector(
ClassLoader classLoader, Logger logger, Player player,
ListenerInvoker invoker, Set<Integer> sendingFilters,
InjectedServerConnection serverInjection) throws IllegalAccessException {
super(logger, player, invoker);
this.classLoader = classLoader;
this.sendingFilters = sendingFilters;
this.serverInjection = serverInjection;
}
@Override
protected boolean hasListener(int packetID) {
return sendingFilters.contains(packetID);
}
@Override
public void initialize() throws IllegalAccessException {
super.initialize();
// Get the send packet method!
if (hasInitialized) {
if (sendPacketMethod == null)
sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*");
}
}
@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;
if (!tryInjectManager()) {
// Try to override the proxied object
if (proxyServerField != null) {
serverHandlerRef = new VolatileField(proxyServerField, serverHandler, true);
serverHandler = serverHandlerRef.getValue();
if (serverHandler == null)
throw new RuntimeException("Cannot hook player: Inner proxy object is NULL.");
// Try again
if (tryInjectManager()) {
// It worked - probably
return;
}
}
throw new RuntimeException(
"Cannot hook player: Unable to find a valid constructor for the NetServerHandler object.");
}
}
private boolean tryInjectManager() {
Class<?> serverClass = serverHandler.getClass();
Enhancer ex = new Enhancer();
Callback sendPacketCallback = new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
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);
};
};
Callback noOpCallback = NoOp.INSTANCE;
ex.setClassLoader(classLoader);
ex.setSuperclass(serverClass);
ex.setCallbacks(new Callback[] { sendPacketCallback, noOpCallback });
ex.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
if (method.equals(sendPacketMethod))
return 0;
else
return 1;
}
});
// Find the Minecraft NetServerHandler superclass
Class<?> minecraftSuperClass = getFirstMinecraftSuperClass(serverHandler.getClass());
ExistingGenerator generator = ExistingGenerator.fromObjectFields(serverHandler, minecraftSuperClass);
DefaultInstances serverInstances = null;
// Maybe the proxy instance can help?
Object proxyInstance = getProxyServerHandler();
// Use the existing server proxy when we create one
if (proxyInstance != null && proxyInstance != serverHandler) {
serverInstances = DefaultInstances.fromArray(generator,
ExistingGenerator.fromObjectArray(new Object[] { proxyInstance }));
} else {
serverInstances = DefaultInstances.fromArray(generator);
}
serverInstances.setNonNull(true);
serverInstances.setMaximumRecursion(1);
Object proxyObject = serverInstances.forEnhancer(ex).getDefault(serverClass);
// Inject it now
if (proxyObject != null) {
// This will be done by InjectedServerConnection instead
//copyTo(serverHandler, proxyObject);
serverInjection.replaceServerHandler(serverHandler, proxyObject);
serverHandlerRef.setValue(proxyObject);
return true;
} else {
return false;
}
}
private Object getProxyServerHandler() {
if (proxyServerField != null && !proxyServerField.equals(serverHandlerRef.getField())) {
try {
return FieldUtils.readField(proxyServerField, serverHandler, true);
} catch (Throwable e) {
// Oh well
}
}
return null;
}
private Class<?> getFirstMinecraftSuperClass(Class<?> clazz) {
if (clazz.getName().startsWith("net.minecraft.server."))
return clazz;
else if (clazz.equals(Object.class))
return clazz;
else
return getFirstMinecraftSuperClass(clazz.getSuperclass());
}
@Override
public void cleanupAll() {
if (serverHandlerRef != null && serverHandlerRef.isCurrentSet()) {
ObjectCloner.copyTo(serverHandlerRef.getValue(), serverHandlerRef.getOldValue(), serverHandler.getClass());
serverHandlerRef.revertValue();
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();
}
}
serverInjection.revertServerHandler(serverHandler);
}
@Override
public void checkListener(PacketListener listener) {
// We support everything
}
@Override
public boolean canInject() {
return true;
}
}
@@ -0,0 +1,311 @@
package com.comphenix.protocol.injector.player;
import java.io.DataInputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.server.Packet;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketListener;
import com.comphenix.protocol.injector.ListenerInvoker;
import com.comphenix.protocol.injector.PlayerLoggedOutException;
import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks;
import com.google.common.collect.ImmutableSet;
/**
* Responsible for injecting into a player's sendPacket method.
*
* @author Kristian
*/
public class PlayerInjectionHandler {
// Server connection injection
private InjectedServerConnection serverInjection;
// The last successful player hook
private PlayerInjector lastSuccessfulHook;
// Player injection
private Map<DataInputStream, Player> connectionLookup = new ConcurrentHashMap<DataInputStream, Player>();
private Map<Player, PlayerInjector> playerInjection = new HashMap<Player, PlayerInjector>();
// Player injection type
private PlayerInjectHooks playerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
// Error logger
private Logger logger;
// Whether or not we're closing
private boolean hasClosed;
// Used to invoke events
private ListenerInvoker invoker;
// Enabled packet filters
private Set<Integer> sendingFilters = Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
// The class loader we're using
private ClassLoader classLoader;
public PlayerInjectionHandler(ClassLoader classLoader, Logger logger, ListenerInvoker invoker, Server server) {
this.classLoader = classLoader;
this.logger = logger;
this.invoker = invoker;
this.serverInjection = new InjectedServerConnection(logger, server);
}
/**
* Retrieves how the server packets are read.
* @return Injection method for reading server packets.
*/
public PlayerInjectHooks getPlayerHook() {
return playerHook;
}
/**
* Add an underlying packet handler of the given ID.
* @param packetID - packet ID to register.
*/
public void addPacketHandler(int packetID) {
sendingFilters.add(packetID);
}
/**
* Remove an underlying packet handler of ths ID.
* @param packetID - packet ID to unregister.
*/
public void removePacketHandler(int packetID) {
sendingFilters.remove(packetID);
}
/**
* Sets how the server packets are read.
* @param playerHook - the new injection method for reading server packets.
*/
public void setPlayerHook(PlayerInjectHooks playerHook) {
this.playerHook = playerHook;
}
/**
* 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.
*/
private PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException {
// Construct the correct player hook
switch (hook) {
case NETWORK_HANDLER_FIELDS:
return new NetworkFieldInjector(classLoader, logger, player, invoker, sendingFilters);
case NETWORK_MANAGER_OBJECT:
return new NetworkObjectInjector(logger, player, invoker, sendingFilters);
case NETWORK_SERVER_OBJECT:
return new NetworkServerInjector(classLoader, logger, player, invoker, sendingFilters, serverInjection);
default:
throw new IllegalArgumentException("Cannot construct a player injector.");
}
}
public Player getPlayerByConnection(DataInputStream inputStream) {
return connectionLookup.get(inputStream);
}
/**
* Initialize a player hook, allowing us to read server packets.
* @param manager - the main packet filter manager.
* @param player - player to hook.
*/
public 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)) {
while (true) {
try {
injector = getHookInstance(player, currentHook);
injector.initialize();
injector.injectManager();
DataInputStream inputStream = injector.getInputStream(false);
if (!player.isOnline() || inputStream == null) {
throw new PlayerLoggedOutException();
}
playerInjection.put(player, injector);
connectionLookup.put(inputStream, player);
break;
} catch (PlayerLoggedOutException e) {
throw e;
} catch (Exception 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);
}
}
/**
* Unregisters the given player.
* @param player - player to unregister.
*/
public void uninjectPlayer(Player player) {
if (!hasClosed && player != null) {
PlayerInjector injector = playerInjection.get(player);
if (injector != null) {
DataInputStream input = injector.getInputStream(true);
injector.cleanupAll();
playerInjection.remove(player);
connectionLookup.remove(input);
}
}
}
public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException {
getInjector(reciever).sendServerPacket(packet.getHandle(), filters);
}
private PlayerInjector getInjector(Player player) {
if (!playerInjection.containsKey(player)) {
// What? Try to inject again.
injectPlayer(player);
}
PlayerInjector injector = playerInjection.get(player);
// Check that the injector was sucessfully added
if (injector != null)
return injector;
else
throw new IllegalArgumentException("Player has no injected handler.");
}
/**
* Determine if the given listeners are valid.
* @param listeners - listeners to check.
*/
public void checkListener(Set<PacketListener> listeners) {
// Make sure the current listeners are compatible
if (lastSuccessfulHook != null) {
for (PacketListener listener : listeners) {
try {
checkListener(listener);
} catch (IllegalStateException e) {
logger.log(Level.WARNING, "Unsupported listener.", e);
}
}
}
}
/**
* 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);
}
}
/**
* Process a packet as if it were sent by the given player.
* @param player - the sender.
* @param mcPacket - the packet to process.
* @throws IllegalAccessException If the reflection machinery failed.
* @throws InvocationTargetException If the underlying method caused an error.
*/
public void processPacket(Player player, Packet mcPacket) throws IllegalAccessException, InvocationTargetException {
PlayerInjector injector = getInjector(player);
injector.processPacket(mcPacket);
}
/**
* Retrieve the current list of registered sending listeners.
* @return List of the sending listeners's packet IDs.
*/
public Set<Integer> getSendingFilters() {
return ImmutableSet.copyOf(sendingFilters);
}
/**
* Retrieve the current logger.
* @return Error logger.
*/
public Logger getLogger() {
return logger;
}
public void close() {
// Guard
if (hasClosed || playerInjection == null)
return;
// Remove everything
for (PlayerInjector injection : playerInjection.values()) {
if (injection != null) {
injection.cleanupAll();
}
}
// Remove server handler
serverInjection.cleanupAll();
hasClosed = true;
playerInjection.clear();
connectionLookup.clear();
invoker = null;
}
}
@@ -15,16 +15,18 @@
* 02111-1307 USA
*/
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.io.DataInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.server.EntityPlayer;
import net.minecraft.server.Packet;
import net.sf.cglib.proxy.Factory;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
@@ -32,6 +34,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.injector.ListenerInvoker;
import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.StructureModifier;
@@ -41,10 +44,15 @@ abstract class PlayerInjector {
// Cache previously retrieved fields
protected static Field serverHandlerField;
protected static Field proxyServerField;
protected static Field networkManagerField;
protected static Field inputField;
protected static Field netHandlerField;
// Whether or not we're using a proxy type
private static boolean hasProxyType;
// To add our injected array lists
protected static StructureModifier<Object> networkModifier;
@@ -65,17 +73,18 @@ abstract class PlayerInjector {
protected Object netHandler;
// The packet manager and filters
protected PacketFilterManager manager;
protected Set<Integer> sendingFilters;
protected ListenerInvoker invoker;
// Previous data input
protected DataInputStream cachedInput;
// Handle errors
protected Logger logger;
public PlayerInjector(Player player, PacketFilterManager manager, Set<Integer> sendingFilters) throws IllegalAccessException {
public PlayerInjector(Logger logger, Player player, ListenerInvoker invoker) throws IllegalAccessException {
this.logger = logger;
this.player = player;
this.manager = manager;
this.sendingFilters = sendingFilters;
initialize();
this.invoker = invoker;
}
/**
@@ -87,7 +96,11 @@ abstract class PlayerInjector {
return craft.getHandle();
}
protected void initialize() throws IllegalAccessException {
/**
* Initialize all fields for this player injector, if it hasn't already.
* @throws IllegalAccessException An error has occured.
*/
public void initialize() throws IllegalAccessException {
EntityPlayer notchEntity = getEntityPlayer();
@@ -96,17 +109,21 @@ abstract class PlayerInjector {
hasInitialized = true;
// Retrieve the server handler
if (serverHandlerField == null)
if (serverHandlerField == null) {
serverHandlerField = FuzzyReflection.fromObject(notchEntity).getFieldByType(".*NetServerHandler");
proxyServerField = getProxyField(notchEntity, serverHandlerField);
}
// Yo dawg
serverHandlerRef = new VolatileField(serverHandlerField, notchEntity);
serverHandler = serverHandlerRef.getValue();
// Next, get the network manager
if (networkManagerField == null)
networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType(".*NetworkManager");
networkManagerRef = new VolatileField(networkManagerField, serverHandler);
networkManager = networkManagerRef.getValue();
// Create the network manager modifier from the actual object type
if (networkManager != null && networkModifier == null)
networkModifier = new StructureModifier<Object>(networkManager.getClass(), null, false);
@@ -123,6 +140,49 @@ abstract class PlayerInjector {
}
}
/**
* Retrieve whether or not the server handler is a proxy object.
* @return TRUE if it is, FALSE otherwise.
*/
protected boolean hasProxyServerHandler() {
return hasProxyType;
}
private Field getProxyField(EntityPlayer notchEntity, Field serverField) {
try {
Object handler = FieldUtils.readField(serverHandlerField, notchEntity, true);
// Is this a Minecraft hook?
if (handler != null && !handler.getClass().getName().startsWith("net.minecraft.server")) {
// This is our proxy object
if (handler instanceof Factory)
return null;
hasProxyType = true;
logger.log(Level.WARNING, "Detected server handler proxy type by another plugin. Conflict may occur!");
// No? Is it a Proxy type?
try {
FuzzyReflection reflection = FuzzyReflection.fromObject(handler, true);
// It might be
return reflection.getFieldByType(".*NetServerHandler");
} catch (RuntimeException e) {
// Damn
}
}
} catch (IllegalAccessException e) {
logger.warning("Unable to load server handler from proxy type.");
}
// Nope, just go with it
return null;
}
/**
* Retrieves the current net handler for this player.
* @return Current net handler.
@@ -205,6 +265,12 @@ abstract class PlayerInjector {
*/
public abstract void cleanupAll();
/**
* Determine if this inject method can even be attempted.
* @return TRUE if can be attempted, though possibly with failure, FALSE otherwise.
*/
public abstract boolean canInject();
/**
* Invoked before a new listener is registered.
* <p>
@@ -218,16 +284,16 @@ abstract class PlayerInjector {
* @param packet - packet to recieve.
* @return The given packet, or the packet replaced by the listeners.
*/
Packet handlePacketRecieved(Packet packet) {
public Packet handlePacketRecieved(Packet packet) {
// Get the packet ID too
Integer id = MinecraftRegistry.getPacketToID().get(packet.getClass());
Integer id = invoker.getPacketID(packet);
// Make sure we're listening
if (id != null && sendingFilters.contains(id)) {
if (id != null && hasListener(id)) {
// A packet has been sent guys!
PacketContainer container = new PacketContainer(id, packet);
PacketEvent event = PacketEvent.fromServer(manager, container, player);
manager.invokePacketSending(event);
PacketEvent event = PacketEvent.fromServer(invoker, container, player);
invoker.invokePacketSending(event);
// Cancelling is pretty simple. Just ignore the packet.
if (event.isCancelled())
@@ -240,12 +306,24 @@ abstract class PlayerInjector {
return packet;
}
/**
* Determine if the given injector is listening for this packet ID.
* @param packetID - packet ID to check.
* @return TRUE if it is, FALSE oterhwise.
*/
protected abstract boolean hasListener(int packetID);
/**
* Retrieve the current player's input stream.
* @param cache - whether or not to cache the result of this method.
* @return The player's input stream.
*/
public DataInputStream getInputStream(boolean cache) {
if (inputField == null)
throw new IllegalStateException("Input field is NULL.");
if (networkManager == null)
throw new IllegalStateException("Network manager is NULL.");
// Get the associated input stream
try {
if (cache && cachedInput != null)
@@ -1,4 +1,4 @@
package com.comphenix.protocol.injector;
package com.comphenix.protocol.injector.player;
import java.util.Collection;
import java.util.List;
@@ -24,10 +24,21 @@ class ReplacedArrayList<TKey> extends ForwardingList<TKey> {
this.underlyingList = underlyingList;
}
/**
* Invoked when a element inserted is replaced.
* @param inserting - the element inserted.
* @param replacement - the element that it should replace.
*/
protected void onReplacing(TKey inserting, TKey replacement) {
// Default is to do nothing.
}
@Override
public boolean add(TKey element) {
if (replaceMap.containsKey(element)) {
return super.add(replaceMap.get(element));
TKey replacement = replaceMap.get(element);
onReplacing(element, replacement);
return super.add(replacement);
} else {
return super.add(element);
}
@@ -36,7 +47,9 @@ class ReplacedArrayList<TKey> extends ForwardingList<TKey> {
@Override
public void add(int index, TKey element) {
if (replaceMap.containsKey(element)) {
super.add(index, replaceMap.get(element));
TKey replacement = replaceMap.get(element);
onReplacing(element, replacement);
super.add(index, replacement);
} else {
super.add(index, element);
}
@@ -101,8 +114,10 @@ class ReplacedArrayList<TKey> extends ForwardingList<TKey> {
*/
public synchronized void replaceAll(TKey find, TKey replace) {
for (int i = 0; i < underlyingList.size(); i++) {
if (Objects.equal(underlyingList.get(i), find))
if (Objects.equal(underlyingList.get(i), find)) {
onReplacing(find, replace);
underlyingList.set(i, replace);
}
}
}
@@ -121,7 +136,9 @@ class ReplacedArrayList<TKey> extends ForwardingList<TKey> {
TKey replaced = underlyingList.get(i);
if (inverse.containsKey(replaced)) {
underlyingList.set(i, inverse.get(replaced));
TKey original = inverse.get(replaced);
onReplacing(replaced, original);
underlyingList.set(i, original);
}
}
@@ -292,7 +292,9 @@ public class FuzzyReflection {
// Like above, only here we test the field type
for (Field field : getFields()) {
if (match.matcher(field.getType().getName()).matches()) {
String name = field.getType().getName();
if (match.matcher(name).matches()) {
return field;
}
}
@@ -0,0 +1,70 @@
package com.comphenix.protocol.reflect;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Can copy an object field by field.
*
* @author Kristian
*/
public class ObjectCloner {
// Cache structure modifiers
@SuppressWarnings("rawtypes")
private static ConcurrentMap<Class, StructureModifier<Object>> cache =
new ConcurrentHashMap<Class, StructureModifier<Object>>();
/**
* Copy every field in object A to object B.
* <p>
* The two objects must have the same number of fields of the same type.
* @param source - fields to copy.
* @param destination - fields to copy to.
* @param commonType - type containing each field to copy.
*/
public static void copyTo(Object source, Object destination, Class<?> commonType) {
if (source == null)
throw new IllegalArgumentException("Source cannot be NULL");
if (destination == null)
throw new IllegalArgumentException("Destination cannot be NULL");
StructureModifier<Object> modifier = cache.get(commonType);
// Create the structure modifier if we haven't already
if (modifier == null) {
StructureModifier<Object> value = new StructureModifier<Object>(commonType, null, false);
modifier = cache.putIfAbsent(commonType, value);
if (modifier == null)
modifier = value;
}
// Add target
StructureModifier<Object> modifierSource = modifier.withTarget(source);
StructureModifier<Object> modifierDest = modifier.withTarget(destination);
// Copy every field
try {
for (int i = 0; i < modifierSource.size(); i++) {
Object value = modifierSource.read(i);
modifierDest.write(i, value);
// System.out.println(String.format("Writing value %s to %s",
// value, modifier.getFields().get(i).getName()));
}
// Copy private fields underneath
Class<?> superclass = commonType.getSuperclass();
if (!superclass.equals(Object.class)) {
copyTo(source, destination, superclass);
}
} catch (FieldAccessException e) {
throw new RuntimeException("Unable to copy fields from " + commonType.getName(), e);
}
}
}
@@ -41,13 +41,18 @@ public class DefaultInstances {
/**
* The maximum height of the hierachy of creates types. Used to prevent cycles.
*/
private final static int MAXIMUM_RECURSION = 20;
private int maximumRecursion = 20;
/**
* Ordered list of instance provider, from highest priority to lowest.
*/
private ImmutableList<InstanceProvider> registered;
/**
* Whether or not the constructor must be non-null.
*/
private boolean nonNull;
/**
* Construct a default instance generator using the given instance providers.
* @param registered - list of instance providers.
@@ -56,6 +61,16 @@ public class DefaultInstances {
this.registered = registered;
}
/**
* Copy a given instance provider.
* @param other - instance provider to copy.
*/
public DefaultInstances(DefaultInstances other) {
this.nonNull = other.nonNull;
this.maximumRecursion = other.maximumRecursion;
this.registered = other.registered;
}
/**
* Construct a default instance generator using the given instance providers.
* @param instaceProviders - array of instance providers.
@@ -81,6 +96,40 @@ public class DefaultInstances {
return registered;
}
/**
* Retrieve whether or not the constructor's parameters must be non-null.
* @return TRUE if they must be non-null, FALSE otherwise.
*/
public boolean isNonNull() {
return nonNull;
}
/**
* Set whether or not the constructor's parameters must be non-null.
* @param nonNull - TRUE if they must be non-null, FALSE otherwise.
*/
public void setNonNull(boolean nonNull) {
this.nonNull = nonNull;
}
/**
* Retrieve the the maximum height of the hierachy of creates types.
* @return Maximum height.
*/
public int getMaximumRecursion() {
return maximumRecursion;
}
/**
* Set the maximum height of the hierachy of creates types. Used to prevent cycles.
* @param maximumRecursion - maximum recursion height.
*/
public void setMaximumRecursion(int maximumRecursion) {
if (maximumRecursion < 1)
throw new IllegalArgumentException("Maxmimum recursion height must be one or higher.");
this.maximumRecursion = maximumRecursion;
}
/**
* Retrieves a default instance or value that is assignable to this type.
* <p>
@@ -107,9 +156,12 @@ public class DefaultInstances {
* @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 <T> Constructor<T> getMinimumConstructor(Class<T> type) {
return getMinimumConstructor(type, registered, 0);
}
@SuppressWarnings("unchecked")
private <T> Constructor<T> getMinimumConstructor(Class<T> type, List<InstanceProvider> providers, int recursionLevel) {
Constructor<T> minimum = null;
int lastCount = Integer.MAX_VALUE;
@@ -121,6 +173,13 @@ public class DefaultInstances {
// require itself in the constructor.
if (types.length < lastCount) {
if (!contains(types, type)) {
if (nonNull) {
// Make sure all of these types are non-null
if (isAnyNull(types, providers, recursionLevel)) {
continue;
}
}
minimum = (Constructor<T>) candidate;
lastCount = types.length;
@@ -134,6 +193,27 @@ public class DefaultInstances {
return minimum;
}
/**
* Determine if any of the given types will be NULL once created.
* <p>
* Recursion level is the number of times the default method has been called.
* @param types - types to check.
* @param providers - instance providers.
* @param recursionLevel - current recursion level.
* @return
*/
private boolean isAnyNull(Class<?>[] types, List<InstanceProvider> providers, int recursionLevel) {
// Just check if any of them are NULL
for (Class<?> type : types) {
if (getDefaultInternal(type, providers, recursionLevel) == null) {
System.out.println(type.getName() + " is NULL!");
return true;
}
}
return false;
}
/**
* Retrieves a default instance or value that is assignable to this type.
* <p>
@@ -149,7 +229,7 @@ public class DefaultInstances {
* </ul>
* </ul>
* @param type - the type to construct a default value.
* @param providers - instance providers used during the
* @param providers - instance providers used during the construction.
* @return A default value/instance, or NULL if not possible.
*/
public <T> T getDefault(Class<T> type, List<InstanceProvider> providers) {
@@ -158,12 +238,8 @@ public class DefaultInstances {
@SuppressWarnings("unchecked")
private <T> T getDefaultInternal(Class<T> type, List<InstanceProvider> providers, int recursionLevel) {
// Guard against recursion
if (recursionLevel > MAXIMUM_RECURSION) {
return null;
}
// The instance providiers should protect themselves against recursion
for (InstanceProvider generator : providers) {
Object value = generator.create(type);
@@ -171,7 +247,12 @@ public class DefaultInstances {
return (T) value;
}
Constructor<T> minimum = getMinimumConstructor(type);
// Guard against recursion
if (recursionLevel >= maximumRecursion) {
return null;
}
Constructor<T> minimum = getMinimumConstructor(type, providers, recursionLevel + 1);
// Create the type with this constructor using default values. This might fail, though.
try {
@@ -183,8 +264,13 @@ public class DefaultInstances {
// Fill out
for (int i = 0; i < parameterCount; i++) {
params[i] = getDefaultInternal(types[i], providers, recursionLevel + 1);
// Did we break the non-null contract?
if (params[i] == null && nonNull) {
return null;
}
}
return createInstance(type, minimum, types, params);
}
@@ -204,7 +290,7 @@ public class DefaultInstances {
public DefaultInstances forEnhancer(Enhancer enhancer) {
final Enhancer ex = enhancer;
return new DefaultInstances(registered) {
return new DefaultInstances(this) {
@SuppressWarnings("unchecked")
@Override
protected <T> T createInstance(Class<T> type, Constructor<T> constructor, Class<?>[] types, Object[] params) {
@@ -17,8 +17,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection;
*/
public class ExistingGenerator implements InstanceProvider {
@SuppressWarnings("rawtypes")
private Map<Class, Object> existingValues = new HashMap<Class, Object>();
private Map<String, Object> existingValues = new HashMap<String, Object>();
private ExistingGenerator() {
// Only accessible to the constructors
@@ -33,13 +32,37 @@ public class ExistingGenerator implements InstanceProvider {
* @return The instance generator.
*/
public static ExistingGenerator fromObjectFields(Object object) {
if (object == null)
throw new IllegalArgumentException("Object cannot be NULL.");
return fromObjectFields(object, object.getClass());
}
/**
* Automatically create an instance provider from a objects public and private fields.
* <p>
* 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.
* @param type - the type to cast the object.
* @return The instance generator.
*/
public static ExistingGenerator fromObjectFields(Object object, Class<?> type) {
ExistingGenerator generator = new ExistingGenerator();
// Possible errors
if (object == null)
throw new IllegalArgumentException("Object cannot be NULL.");
if (type == null)
throw new IllegalArgumentException("Type cannot be NULL.");
if (!type.isAssignableFrom(object.getClass()))
throw new IllegalArgumentException("Type must be a superclass or be the same type.");
// Read instances from every field.
for (Field field : FuzzyReflection.fromObject(object, true).getFields()) {
for (Field field : FuzzyReflection.fromClass(type, 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);
@@ -48,7 +71,7 @@ public class ExistingGenerator implements InstanceProvider {
// Yes, swallow it. No, really.
}
}
return generator;
}
@@ -70,19 +93,18 @@ public class ExistingGenerator implements InstanceProvider {
if (value == null)
throw new IllegalArgumentException("Value cannot be NULL.");
existingValues.put(value.getClass(), value);
existingValues.put(value.getClass().getName(), value);
}
private void addObject(Class<?> type, Object value) {
existingValues.put(type, value);
existingValues.put(type.getName(), value);
}
@Override
public Object create(@Nullable Class<?> type) {
Object value = existingValues.get(type);
Object value = existingValues.get(type.getName());
// NULL values indicate that the generator failed
return value;
}
+1 -1
View File
@@ -1,5 +1,5 @@
name: ProtocolLib
version: 1.2.0
version: 1.3.0
description: Provides read/write access to the Minecraft protocol.
author: Comphenix
website: http://www.comphenix.net/ProtocolLib