diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml
index 5897f7c6..1b769aac 100644
--- a/ProtocolLib/pom.xml
+++ b/ProtocolLib/pom.xml
@@ -2,7 +2,7 @@
4.0.0
com.comphenix.protocol
ProtocolLib
- 1.7.0
+ 1.7.1
jar
Provides read/write access to the Minecraft protocol.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
index ffe6b34d..e3f8e09d 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java
@@ -23,6 +23,8 @@ import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.bukkit.Server;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@@ -187,8 +189,8 @@ public class ProtocolLibrary extends JavaPlugin {
}
// Set up command handlers
- getCommand(CommandProtocol.NAME).setExecutor(commandProtocol);
- getCommand(CommandPacket.NAME).setExecutor(commandPacket);
+ registerCommand(CommandProtocol.NAME, commandProtocol);
+ registerCommand(CommandPacket.NAME, commandPacket);
// Notify server managers of incompatible plugins
checkForIncompatibility(manager);
@@ -217,6 +219,24 @@ public class ProtocolLibrary extends JavaPlugin {
reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc);
}
}
+
+ private void registerCommand(String name, CommandExecutor executor) {
+ try {
+ if (executor == null)
+ throw new RuntimeException("Executor was NULL.");
+
+ PluginCommand command = getCommand(name);
+
+ // Try to load the command
+ if (command != null)
+ command.setExecutor(executor);
+ else
+ throw new RuntimeException("plugin.yml might be corrupt.");
+
+ } catch (RuntimeException e) {
+ reporter.reportWarning(this, "Cannot register command " + name + ": " + e.getMessage());
+ }
+ }
/**
* Disable the current plugin.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
index 3e319249..741c54bc 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolManager.java
@@ -130,7 +130,7 @@ public interface ProtocolManager extends PacketStream {
public PacketConstructor createPacketConstructor(int id, Object... arguments);
/**
- * Completely refresh all clients about an entity.
+ * Completely resend an entity to a list of clients.
*
* Note that this method is NOT thread safe. If you call this method from anything
* but the main thread, it will throw an exception.
@@ -148,6 +148,14 @@ public interface ProtocolManager extends PacketStream {
*/
public Entity getEntityFromID(World container, int id) throws FieldAccessException;
+ /**
+ * Retrieve every client that is receiving information about a given entity.
+ * @param entity - the entity that is being tracked.
+ * @return Every client/player that is tracking the given entity.
+ * @throws FieldAccessException If reflection failed.
+ */
+ public List getEntityTrackers(Entity entity) throws FieldAccessException;
+
/**
* Retrieves a immutable set containing the ID of the sent server packets that will be observed by listeners.
* @return Every filtered server packet.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java
index 571083d5..9b241e90 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/AbstractIntervalTree.java
@@ -132,7 +132,7 @@ public abstract class AbstractIntervalTree, TValue
* Removes every interval that intersects with the given range.
* @param lowerBound - lowest value to remove.
* @param upperBound - highest value to remove.
- * @param preserveOutside - whether or not to preserve the intervals that are partially outside.
+ * @param preserveDifference - whether or not to preserve the intervals that are partially outside.
*/
public Set remove(TKey lowerBound, TKey upperBound, boolean preserveDifference) {
checkBounds(lowerBound, upperBound);
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java
new file mode 100644
index 00000000..793f3c11
--- /dev/null
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java
@@ -0,0 +1,165 @@
+package com.comphenix.protocol.concurrency;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.MapMaker;
+
+/**
+ * A map that supports blocking on read operations. Null keys are not supported.
+ *
+ * Keys are stored as weak references, and will be automatically removed once they've all been dereferenced.
+ *
+ * @author Kristian
+ *
+ * @param - type of the key.
+ * @param - type of the value.
+ */
+public class BlockingHashMap {
+
+ // Map of values
+ private final ConcurrentMap backingMap;
+
+ // Map of locked objects
+ private final ConcurrentMap locks;
+
+ /**
+ * Initialize a new map.
+ */
+ public BlockingHashMap() {
+ backingMap = new MapMaker().weakKeys().makeMap();
+ locks = new MapMaker().weakKeys().makeMap();
+ }
+
+ /**
+ * Initialize a new map.
+ * @return The created map.
+ */
+ public static BlockingHashMap create() {
+ return new BlockingHashMap();
+ }
+
+ /**
+ * Waits until a value has been associated with the given key, and then retrieves that value.
+ * @param key - the key whose associated value is to be returned
+ * @return The value to which the specified key is mapped.
+ * @throws InterruptedException If the current thread got interrupted while waiting.
+ */
+ public TValue get(TKey key) throws InterruptedException {
+ if (key == null)
+ throw new IllegalArgumentException("key cannot be NULL.");
+
+ TValue value = backingMap.get(key);
+
+ // Only lock if no value is available
+ if (value == null) {
+ final Object lock = getLock(key);
+
+ synchronized (lock) {
+ while (value == null) {
+ lock.wait();
+ value = backingMap.get(key);
+ }
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Waits until a value has been associated with the given key, and then retrieves that value.
+ * @param key - the key whose associated value is to be returned
+ * @param timeout - the amount of time to wait until an association has been made.
+ * @param unit - unit of timeout.
+ * @return The value to which the specified key is mapped, or NULL if the timeout elapsed.
+ * @throws InterruptedException If the current thread got interrupted while waiting.
+ */
+ public TValue get(TKey key, long timeout, TimeUnit unit) throws InterruptedException {
+ if (key == null)
+ throw new IllegalArgumentException("key cannot be NULL.");
+ if (unit == null)
+ throw new IllegalArgumentException("Unit cannot be NULL.");
+
+ TValue value = backingMap.get(key);
+
+ // Only lock if no value is available
+ if (value == null) {
+ final Object lock = getLock(key);
+ final long stopTimeNS = System.nanoTime() + unit.toNanos(timeout);
+
+ // Don't exceed the timeout
+ synchronized (lock) {
+ while (value == null) {
+ long remainingTime = stopTimeNS - System.nanoTime();
+
+ if (remainingTime > 0) {
+ TimeUnit.NANOSECONDS.timedWait(lock, remainingTime);
+ value = backingMap.get(key);
+ }
+ }
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Associate a given key with the given value.
+ *
+ * Wakes up any blocking getters on this specific key.
+ *
+ * @param key - the key to associate.
+ * @param value - the value.
+ * @return The previously associated value.
+ */
+ public TValue put(TKey key, TValue value) {
+ if (value == null)
+ throw new IllegalArgumentException("This map doesn't support NULL values.");
+
+ final TValue previous = backingMap.put(key, value);
+ final Object lock = getLock(key);
+
+ // Inform our readers about this change
+ synchronized (lock) {
+ lock.notifyAll();
+ return previous;
+ }
+ }
+
+ public int size() {
+ return backingMap.size();
+ }
+
+ public Collection values() {
+ return backingMap.values();
+ }
+
+ public Set keys() {
+ return backingMap.keySet();
+ }
+
+ /**
+ * Atomically retrieve the lock associated with a given key.
+ * @param key - the current key.
+ * @return An asssociated lock.
+ */
+ private Object getLock(TKey key) {
+ Object lock = locks.get(key);
+
+ if (lock == null) {
+ Object created = new Object();
+
+ // Do this atomically
+ lock = locks.putIfAbsent(key, created);
+
+ // If we succeeded, use the latch we created - otherwise, use the already inserted latch
+ if (lock == null) {
+ lock = created;
+ }
+ }
+
+ return lock;
+ }
+}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java
index 4150acba..2ff67143 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java
@@ -17,6 +17,8 @@
package com.comphenix.protocol.events;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -193,6 +195,14 @@ public class PacketContainer implements Serializable {
public StructureModifier getByteArrays() {
return structureModifier.withType(byte[].class);
}
+
+ /**
+ * Retrieves a read/write structure for every int array field.
+ * @return A modifier for every int array field.
+ */
+ public StructureModifier getIntegerArrays() {
+ return structureModifier.withType(int[].class);
+ }
/**
* Retrieves a read/write structure for ItemStack.
@@ -347,6 +357,42 @@ public class PacketContainer implements Serializable {
return id;
}
+ /**
+ * Create a deep copy of the current packet.
+ * @return A deep copy of the current packet.
+ */
+ public PacketContainer deepClone() {
+ ObjectOutputStream output = null;
+ ObjectInputStream input = null;
+
+ try {
+ // Use a small buffer of 32 bytes initially.
+ ByteArrayOutputStream bufferOut = new ByteArrayOutputStream();
+ output = new ObjectOutputStream(bufferOut);
+ output.writeObject(this);
+
+ ByteArrayInputStream bufferIn = new ByteArrayInputStream(bufferOut.toByteArray());
+ input = new ObjectInputStream(bufferIn);
+ return (PacketContainer) input.readObject();
+
+ } catch (IOException e) {
+ throw new IllegalStateException("Unexpected error occured during object cloning.", e);
+ } catch (ClassNotFoundException e) {
+ // Cannot happen
+ throw new IllegalStateException("Unexpected failure with serialization.", e);
+ } finally {
+ try {
+ if (output != null)
+ output.close();
+ if (input != null)
+ input.close();
+
+ } catch (IOException e) {
+ // STOP IT
+ }
+ }
+ }
+
private void writeObject(ObjectOutputStream output) throws IOException {
// Default serialization
output.defaultWriteObject();
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java
index 9671c6ce..ef9c9e8c 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java
@@ -21,12 +21,14 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import net.minecraft.server.EntityPlayer;
import net.minecraft.server.EntityTrackerEntry;
import org.bukkit.World;
@@ -118,7 +120,39 @@ class EntityUtilities {
} catch (SecurityException e) {
throw new FieldAccessException("Security limitation prevents access to 'scanPlayers' method in trackerEntry.", e);
} catch (NoSuchMethodException e) {
- throw new FieldAccessException("Canot find 'scanPlayers' method. Is ProtocolLib up to date?", e);
+ throw new FieldAccessException("Cannot find 'scanPlayers' method. Is ProtocolLib up to date?", e);
+ }
+ }
+
+ /**
+ * Retrieve every client that is receiving information about a given entity.
+ * @param entity - the entity that is being tracked.
+ * @return Every client/player that is tracking the given entity.
+ * @throws FieldAccessException If reflection failed.
+ */
+ public static List getEntityTrackers(Entity entity) {
+ try {
+ List result = new ArrayList();
+ Object trackerEntry = getEntityTrackerEntry(entity.getWorld(), entity.getEntityId());
+
+ if (trackedPlayersField == null)
+ trackedPlayersField = FuzzyReflection.fromObject(trackerEntry).getFieldByType("java\\.util\\..*");
+
+ Collection> trackedPlayers = (Collection>) FieldUtils.readField(trackedPlayersField, trackerEntry, false);
+
+ // Wrap every player - we also ensure that the underlying tracker list is immutable
+ for (Object tracker : trackedPlayers) {
+ if (tracker instanceof EntityPlayer) {
+ EntityPlayer nmsPlayer = (EntityPlayer) tracker;
+ result.add(nmsPlayer.getBukkitEntity());
+ }
+ }
+ return result;
+
+ } catch (IllegalAccessException e) {
+ throw new FieldAccessException("Security limitation prevented access to the list of tracked players.", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldAccessException("Exception occurred in Minecraft.", e);
}
}
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java
index a8706e90..27f7c7c0 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java
@@ -571,6 +571,11 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok
return EntityUtilities.getEntityFromID(container, id);
}
+ @Override
+ public List getEntityTrackers(Entity entity) throws FieldAccessException {
+ return EntityUtilities.getEntityTrackers(entity);
+ }
+
/**
* Initialize the packet injection for every player.
* @param players - list of players to inject.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java
index 7630c360..8d3374dc 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java
@@ -202,13 +202,20 @@ class PacketInjector {
// Called from the ReadPacketModified monitor
PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) {
- Player client = playerInjection.getPlayerByConnection(input);
-
- // Never invoke a event if we don't know where it's from
- if (client != null)
- return packetRecieved(packet, client);
- else
+ try {
+ Player client = playerInjection.getPlayerByConnection(input);
+
+ // Never invoke a event if we don't know where it's from
+ if (client != null)
+ return packetRecieved(packet, client);
+ else
+ return null;
+
+ } catch (InterruptedException e) {
+ // We will ignore this - it occurs when a player disconnects
+ //reporter.reportDetailed(this, "Thread was interrupted.", e, packet, input);
return null;
+ }
}
/**
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java
index ac1c16d6..64e2f005 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetLoginInjector.java
@@ -1,9 +1,6 @@
package com.comphenix.protocol.injector.player;
import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.bukkit.Server;
import org.bukkit.entity.Player;
@@ -29,8 +26,6 @@ class NetLoginInjector {
// The current error rerporter
private ErrorReporter reporter;
- private ReadWriteLock injectionLock = new ReentrantReadWriteLock();
-
// Used to create fake players
private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory();
@@ -46,9 +41,6 @@ class NetLoginInjector {
* @return An injected NetLoginHandler, or the original object.
*/
public Object onNetLoginCreated(Object inserting) {
-
- injectionLock.writeLock().lock();
-
try {
// Make sure we actually need to inject during this phase
if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN))
@@ -73,19 +65,9 @@ class NetLoginInjector {
reporter.reportDetailed(this, "Unable to hook NetLoginHandler.", e, inserting);
return inserting;
- } finally {
- injectionLock.writeLock().unlock();
}
}
- /**
- * Retrieve the lock used for reading.
- * @return Reading lock.
- */
- public Lock getReadLock() {
- return injectionLock.readLock();
- }
-
/**
* Invoked when a NetLoginHandler should be reverted.
* @param inserting - the original NetLoginHandler.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java
index fb2136b3..c5f832b9 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectionHandler.java
@@ -24,12 +24,14 @@ import java.net.Socket;
import java.net.SocketAddress;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import net.minecraft.server.Packet;
import org.bukkit.Server;
import org.bukkit.entity.Player;
+import com.comphenix.protocol.concurrency.BlockingHashMap;
import com.comphenix.protocol.error.ErrorReporter;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
@@ -47,6 +49,10 @@ import com.google.common.collect.Maps;
* @author Kristian
*/
public class PlayerInjectionHandler {
+ /**
+ * The maximum number of milliseconds to wait until a player can be looked up by connection.
+ */
+ private static final long TIMEOUT_PLAYER_LOOKUP = 1000; // ms
/**
* The highest possible packet ID. It's unlikely that this value will ever change.
@@ -64,9 +70,11 @@ public class PlayerInjectionHandler {
// Player injection
private Map addressLookup = Maps.newConcurrentMap();
- private Map dataInputLookup = Maps.newConcurrentMap();
private Map playerInjection = Maps.newConcurrentMap();
+ // Lookup player by connection
+ private BlockingHashMap dataInputLookup = BlockingHashMap.create();
+
// Player injection types
private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT;
@@ -193,26 +201,37 @@ public class PlayerInjectionHandler {
* Retrieve a player by its DataInput connection.
* @param inputStream - the associated DataInput connection.
* @return The player.
+ * @throws InterruptedException If the thread was interrupted during the wait.
*/
- public Player getPlayerByConnection(DataInputStream inputStream) {
- try {
- // Concurrency issue!
- netLoginInjector.getReadLock().lock();
-
- PlayerInjector injector = dataInputLookup.get(inputStream);
-
- if (injector != null) {
- return injector.getPlayer();
- } else {
- reporter.reportWarning(this, "Unable to find stream: " + inputStream);
- return null;
- }
-
- } finally {
- netLoginInjector.getReadLock().unlock();
+ public Player getPlayerByConnection(DataInputStream inputStream) throws InterruptedException {
+ return getPlayerByConnection(inputStream, TIMEOUT_PLAYER_LOOKUP, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Retrieve a player by its DataInput connection.
+ * @param inputStream - the associated DataInput connection.
+ * @param playerTimeout - the amount of time to wait for a result.
+ * @param unit - unit of playerTimeout.
+ * @return The player.
+ * @throws InterruptedException If the thread was interrupted during the wait.
+ */
+ public Player getPlayerByConnection(DataInputStream inputStream, long playerTimeout, TimeUnit unit) throws InterruptedException {
+ // Wait until the connection owner has been established
+ PlayerInjector injector = dataInputLookup.get(inputStream, playerTimeout, unit);
+
+ if (injector != null) {
+ return injector.getPlayer();
+ } else {
+ reporter.reportWarning(this, "Unable to find stream: " + inputStream);
+ return null;
}
}
+ /**
+ * Helper function that retrieves the injector type of a given player injector.
+ * @param injector - injector type.
+ * @return The injector type.
+ */
private PlayerInjectHooks getInjectorType(PlayerInjector injector) {
return injector != null ? injector.getHookType() : PlayerInjectHooks.NONE;
}
@@ -404,7 +423,6 @@ public class PlayerInjectionHandler {
PlayerInjector injector = playerInjection.remove(player);
if (injector != null) {
- DataInputStream input = injector.getInputStream(true);
InetSocketAddress address = player.getAddress();
injector.cleanupAll();
@@ -423,8 +441,7 @@ public class PlayerInjectionHandler {
// Clean up
if (removeAuxiliary) {
- if (input != null)
- dataInputLookup.remove(input);
+ // Note that the dataInputLookup will clean itself
if (address != null)
addressLookup.remove(address);
}
@@ -571,7 +588,6 @@ public class PlayerInjectionHandler {
}
public void close() {
-
// Guard
if (hasClosed || playerInjection == null)
return;
@@ -593,7 +609,6 @@ public class PlayerInjectionHandler {
hasClosed = true;
playerInjection.clear();
- dataInputLookup.clear();
addressLookup.clear();
invoker = null;
}
@@ -607,12 +622,9 @@ public class PlayerInjectionHandler {
// Update the DataInputStream
if (injector != null) {
- final DataInputStream old = injector.getInputStream(true);
-
injector.scheduleAction(new Runnable() {
@Override
public void run() {
- dataInputLookup.remove(old);
dataInputLookup.put(injector.getInputStream(false), injector);
}
});
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FieldAccessException.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FieldAccessException.java
index 1d1c13c2..3abec315 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FieldAccessException.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FieldAccessException.java
@@ -22,7 +22,7 @@ package com.comphenix.protocol.reflect;
*
* @author Kristian
*/
-public class FieldAccessException extends Exception {
+public class FieldAccessException extends RuntimeException {
/**
* Generated by Eclipse.
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java
index 5f39621e..0342e47c 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java
@@ -1,6 +1,7 @@
package com.comphenix.protocol.wrappers;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@@ -21,7 +22,6 @@ import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.base.Objects;
-import net.minecraft.server.ChunkCoordinates;
import net.minecraft.server.DataWatcher;
import net.minecraft.server.WatchableObject;
@@ -116,8 +116,7 @@ public class WrappedDataWatcher {
*/
public static Integer getTypeID(Class> clazz) throws FieldAccessException {
initialize();
-
- return typeMap.get(clazz);
+ return typeMap.get(WrappedWatchableObject.getUnwrappedType(clazz));
}
/**
@@ -145,7 +144,7 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public Byte getByte(int index) throws FieldAccessException {
- return (Byte) getObjectRaw(index);
+ return (Byte) getObject(index);
}
/**
@@ -155,7 +154,7 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public Short getShort(int index) throws FieldAccessException {
- return (Short) getObjectRaw(index);
+ return (Short) getObject(index);
}
/**
@@ -165,7 +164,7 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public Integer getInteger(int index) throws FieldAccessException {
- return (Integer) getObjectRaw(index);
+ return (Integer) getObject(index);
}
/**
@@ -175,7 +174,7 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public Float getFloat(int index) throws FieldAccessException {
- return (Float) getObjectRaw(index);
+ return (Float) getObject(index);
}
/**
@@ -185,7 +184,7 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public String getString(int index) throws FieldAccessException {
- return (String) getObjectRaw(index);
+ return (String) getObject(index);
}
/**
@@ -215,25 +214,6 @@ public class WrappedDataWatcher {
* @throws FieldAccessException Cannot read underlying field.
*/
public Object getObject(int index) throws FieldAccessException {
- Object result = getObjectRaw(index);
-
- // Handle the special cases too
- if (result instanceof net.minecraft.server.ItemStack) {
- return BukkitConverters.getItemStackConverter().getSpecific(result);
- } else if (result instanceof ChunkCoordinates) {
- return new WrappedChunkCoordinate((ChunkCoordinates) result);
- } else {
- return result;
- }
- }
-
- /**
- * Retrieve a watchable object by index.
- * @param index - index of the object to retrieve.
- * @return The watched object.
- * @throws FieldAccessException Cannot read underlying field.
- */
- private Object getObjectRaw(int index) throws FieldAccessException {
// The get method will take care of concurrency
WatchableObject watchable = getWatchedObject(index);
@@ -319,27 +299,7 @@ public class WrappedDataWatcher {
* @param update - whether or not to refresh every listening clients.
* @throws FieldAccessException Cannot read underlying field.
*/
- public void setObject(int index, Object newValue, boolean update) throws FieldAccessException {
- // Convert special cases
- if (newValue instanceof WrappedChunkCoordinate)
- newValue = ((WrappedChunkCoordinate) newValue).getHandle();
- if (newValue instanceof ItemStack)
- newValue = BukkitConverters.getItemStackConverter().getGeneric(
- net.minecraft.server.ItemStack.class, (ItemStack) newValue);
-
- // Next, set the object
- setObjectRaw(index, newValue, update);
- }
-
- /**
- * Set a watchable object by index.
- * @param index - index of the object to retrieve.
- * @param newValue - the new watched value.
- * @param update - whether or not to refresh every listening clients.
- * @return The watched object.
- * @throws FieldAccessException Cannot read underlying field.
- */
- private void setObjectRaw(int index, Object newValue, boolean update) throws FieldAccessException {
+ public void setObject(int index, Object newValue, boolean update) throws FieldAccessException {
// Aquire write lock
Lock writeLock = getReadWriteLock().writeLock();
writeLock.lock();
@@ -349,12 +309,22 @@ public class WrappedDataWatcher {
if (watchable != null) {
new WrappedWatchableObject(watchable).setValue(newValue, update);
+ } else {
+ createKeyValueMethod.invoke(handle, index, WrappedWatchableObject.getUnwrapped(newValue));
}
- } finally {
+
+ // Handle invoking the method
+ } catch (IllegalArgumentException e) {
+ throw new FieldAccessException("Cannot convert arguments.", e);
+ } catch (IllegalAccessException e) {
+ throw new FieldAccessException("Illegal access.", e);
+ } catch (InvocationTargetException e) {
+ throw new FieldAccessException("Checked exception in Minecraft.", e);
+ } finally {
writeLock.unlock();
}
}
-
+
private WatchableObject getWatchedObject(int index) throws FieldAccessException {
// We use the get-method first and foremost
if (getKeyValueMethod != null) {
diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java
index d9862029..2124d2a3 100644
--- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java
+++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedWatchableObject.java
@@ -5,6 +5,7 @@ import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.reflect.instances.DefaultInstances;
+import net.minecraft.server.ChunkCoordinates;
import net.minecraft.server.ItemStack;
import net.minecraft.server.WatchableObject;
@@ -27,7 +28,35 @@ public class WrappedWatchableObject {
// Type of the stored value
private Class> typeClass;
+ /**
+ * Wrap a given raw Minecraft watchable object.
+ * @param handle - the raw watchable object to wrap.
+ */
public WrappedWatchableObject(WatchableObject handle) {
+ load(handle);
+ }
+
+ /**
+ * Construct a watchable object from an index and a given value.
+ * @param index - the index.
+ * @param value - non-null value of specific types.
+ */
+ public WrappedWatchableObject(int index, Object value) {
+ if (value == null)
+ throw new IllegalArgumentException("Value cannot be NULL.");
+
+ // Get the correct type ID
+ Integer typeID = WrappedDataWatcher.getTypeID(value.getClass());
+
+ if (typeID != null) {
+ load(new WatchableObject(typeID, index, getUnwrapped(value)));
+ } else {
+ throw new IllegalArgumentException("Cannot watch the type " + value.getClass());
+ }
+ }
+
+ // Wrap a NMS object
+ private void load(WatchableObject handle) {
initialize();
this.handle = handle;
this.modifier = baseModifier.withTarget(handle);
@@ -57,6 +86,15 @@ public class WrappedWatchableObject {
* @throws FieldAccessException Unable to read values.
*/
public Class> getType() throws FieldAccessException {
+ return getWrappedType(getTypeRaw());
+ }
+
+ /**
+ * Retrieve the correct super type of the current value, given the raw NMS object.
+ * @return Super type.
+ * @throws FieldAccessException Unable to read values.
+ */
+ private Class> getTypeRaw() throws FieldAccessException {
if (typeClass == null) {
typeClass = WrappedDataWatcher.getTypeClass(getTypeID());
@@ -131,7 +169,7 @@ public class WrappedWatchableObject {
setDirtyState(true);
// Use the modifier to set the value
- modifier.withType(Object.class).write(0, newValue);
+ modifier.withType(Object.class).write(0, getUnwrapped(newValue));
}
/**
@@ -140,7 +178,7 @@ public class WrappedWatchableObject {
* @throws FieldAccessException Unable to use reflection.
*/
public Object getValue() throws FieldAccessException {
- return modifier.withType(Object.class).read(0);
+ return getWrapped(modifier.withType(Object.class).read(0));
}
/**
@@ -161,6 +199,66 @@ public class WrappedWatchableObject {
return modifier.withType(boolean.class).read(0);
}
+ /**
+ * Retrieve the wrapped object value, if needed.
+ * @param value - the raw NMS object to wrap.
+ * @return The wrapped object.
+ */
+ static Object getWrapped(Object value) {
+ // Handle the special cases
+ if (value instanceof net.minecraft.server.ItemStack) {
+ return BukkitConverters.getItemStackConverter().getSpecific(value);
+ } else if (value instanceof ChunkCoordinates) {
+ return new WrappedChunkCoordinate((ChunkCoordinates) value);
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * Retrieve the wrapped type, if needed.
+ * @param wrapped - the wrapped class type.
+ * @return The wrapped class type.
+ */
+ static Class> getWrappedType(Class> unwrapped) {
+ if (unwrapped.equals(net.minecraft.server.ChunkPosition.class))
+ return ChunkPosition.class;
+ else if (unwrapped.equals(ChunkCoordinates.class))
+ return WrappedChunkCoordinate.class;
+ else
+ return unwrapped;
+ }
+
+ /**
+ * Retrieve the raw NMS value.
+ * @param wrapped - the wrapped position.
+ * @return The raw NMS object.
+ */
+ static Object getUnwrapped(Object wrapped) {
+ // Convert special cases
+ if (wrapped instanceof WrappedChunkCoordinate)
+ return ((WrappedChunkCoordinate) wrapped).getHandle();
+ else if (wrapped instanceof ItemStack)
+ return BukkitConverters.getItemStackConverter().getGeneric(
+ net.minecraft.server.ItemStack.class, (org.bukkit.inventory.ItemStack) wrapped);
+ else
+ return wrapped;
+ }
+
+ /**
+ * Retrieve the unwrapped type, if needed.
+ * @param wrapped - the unwrapped class type.
+ * @return The unwrapped class type.
+ */
+ static Class> getUnwrappedType(Class> wrapped) {
+ if (wrapped.equals(ChunkPosition.class))
+ return net.minecraft.server.ChunkPosition.class;
+ else if (wrapped.equals(WrappedChunkCoordinate.class))
+ return ChunkCoordinates.class;
+ else
+ return wrapped;
+ }
+
/**
* Clone the current wrapped watchable object, along with any contained objects.
* @return A deep clone of the current watchable object.
diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml
index d632e814..69f13146 100644
--- a/ProtocolLib/src/main/resources/plugin.yml
+++ b/ProtocolLib/src/main/resources/plugin.yml
@@ -1,5 +1,5 @@
name: ProtocolLib
-version: 1.7.0
+version: 1.7.1
description: Provides read/write access to the Minecraft protocol.
author: Comphenix
website: http://www.comphenix.net/ProtocolLib
diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/concurrency/BlockingHashMapTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/concurrency/BlockingHashMapTest.java
new file mode 100644
index 00000000..7be7e705
--- /dev/null
+++ b/ProtocolLib/src/test/java/com/comphenix/protocol/concurrency/BlockingHashMapTest.java
@@ -0,0 +1,42 @@
+package com.comphenix.protocol.concurrency;
+
+import static org.junit.Assert.*;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.junit.Test;
+
+public class BlockingHashMapTest {
+
+ @Test
+ public void test() throws InterruptedException, ExecutionException {
+
+ final BlockingHashMap map = BlockingHashMap.create();
+
+ ExecutorService service = Executors.newSingleThreadExecutor();
+
+ // Create a reader
+ Future future = service.submit(new Callable() {
+ @Override
+ public String call() throws Exception {
+ // Combine for easy reading
+ return map.get(0) + map.get(1);
+ }
+ });
+
+ // Wait a bit
+ Thread.sleep(50);
+
+ // Insert values
+ map.put(0, "hello ");
+ map.put(1, "world");
+
+ // Wait for the other thread to complete
+ assertEquals(future.get(), "hello world");
+ }
+
+}