diff --git a/ItemDisguise/.classpath b/ItemDisguise/.classpath index 2bda6dc7..71e70473 100644 --- a/ItemDisguise/.classpath +++ b/ItemDisguise/.classpath @@ -7,12 +7,6 @@ - - - - - - diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index e1268256..ac7a8b99 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.2.0 + 2.3.0 jar Provides read/write access to the Minecraft protocol. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java index 57f31fe7..dd7c5bbe 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -27,6 +27,9 @@ import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.MethodUtils; @@ -36,9 +39,11 @@ import com.comphenix.protocol.reflect.compiler.StructureCompiler; import com.comphenix.protocol.reflect.instances.CollectionGenerator; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.reflect.instances.PrimitiveGenerator; +import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.ChunkPosition; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedWatchableObject; +import com.comphenix.protocol.wrappers.nbt.io.NbtBinarySerializer; /** * Used to fix ClassLoader leaks that may lead to filling up the permanent generation. @@ -66,7 +71,9 @@ class CleanupStaticMembers { PrimitiveGenerator.class, FuzzyReflection.class, MethodUtils.class, BackgroundCompiler.class, StructureCompiler.class, ObjectWriter.class, Packets.Server.class, Packets.Client.class, - ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class + ChunkPosition.class, WrappedDataWatcher.class, WrappedWatchableObject.class, + AbstractInputStreamLookup.class, TemporaryPlayerFactory.class, SpigotPacketInjector.class, + MinecraftReflection.class, NbtBinarySerializer.class }; String[] internalClasses = { @@ -76,14 +83,14 @@ class CleanupStaticMembers { "com.comphenix.protocol.injector.player.NetworkObjectInjector", "com.comphenix.protocol.injector.player.NetworkServerInjector", "com.comphenix.protocol.injector.player.PlayerInjector", - "com.comphenix.protocol.injector.player.TemporaryPlayerFactory", "com.comphenix.protocol.injector.EntityUtilities", "com.comphenix.protocol.injector.packet.PacketRegistry", "com.comphenix.protocol.injector.packet.PacketInjector", "com.comphenix.protocol.injector.packet.ReadPacketModifier", "com.comphenix.protocol.injector.StructureCache", "com.comphenix.protocol.reflect.compiler.BoxingHelper", - "com.comphenix.protocol.reflect.compiler.MethodDescriptor" + "com.comphenix.protocol.reflect.compiler.MethodDescriptor", + "com.comphenix.protocol.wrappers.nbt.WrappedElement", }; resetClasses(publicClasses); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java index ce084d0a..87e5b523 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolConfig.java @@ -228,7 +228,7 @@ class ProtocolConfig { public void setBackgroundCompilerEnabled(boolean enabled) { global.set(BACKGROUND_COMPILER_ENABLED, enabled); } - + /** * Set the last time we updated, in seconds since 1970.01.01 00:00. * @param lastTimeSeconds - new last update time. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 28c1ef9d..271c1181 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -40,7 +40,9 @@ import com.comphenix.protocol.injector.PacketFilterManager; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; import com.comphenix.protocol.metrics.Statistics; import com.comphenix.protocol.metrics.Updater; +import com.comphenix.protocol.metrics.Updater.UpdateResult; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; +import com.comphenix.protocol.utility.ChatExtensions; /** * The main entry point for ProtocolLib. @@ -134,7 +136,10 @@ public class ProtocolLibrary extends JavaPlugin { updater = new Updater(this, logger, "protocollib", getFile(), "protocol.info"); unhookTask = new DelayedSingleTask(this); - protocolManager = new PacketFilterManager(getClassLoader(), getServer(), unhookTask, detailedReporter); + protocolManager = new PacketFilterManager( + getClassLoader(), getServer(), unhookTask, detailedReporter); + + // Setup error reporter detailedReporter.addGlobalParameter("manager", protocolManager); // Update injection hook @@ -215,6 +220,24 @@ public class ProtocolLibrary extends JavaPlugin { // Don't do anything else! if (manager == null) return; + // Silly plugin reloaders! + if (protocolManager == null) { + Logger directLogging = Logger.getLogger("Minecraft"); + String[] message = new String[] { + " PROTOCOLLIB DOES NOT SUPPORT PLUGIN RELOADERS. ", + " PLEASE USE THE BUILT-IN RELOAD COMMAND. ", + }; + + // Print as severe + for (String line : ChatExtensions.toFlowerBox(message, "*", 3, 1)) { + directLogging.severe(line); + } + disablePlugin(); + return; + } + + // Perform logic when the world has loaded + protocolManager.postWorldLoaded(); // Initialize background compiler if (backgroundCompiler == null && config.isBackgroundCompilerEnabled()) { @@ -257,7 +280,7 @@ public class ProtocolLibrary extends JavaPlugin { reporter.reportDetailed(this, "Metrics cannot be enabled. Incompatible Bukkit version.", e, statistisc); } } - + // Used to check Minecraft version private void verifyMinecraftVersion() { try { @@ -422,16 +445,22 @@ public class ProtocolLibrary extends JavaPlugin { if (redirectHandler != null) { logger.removeHandler(redirectHandler); } - - unhookTask.close(); - protocolManager.close(); + if (protocolManager != null) + protocolManager.close(); + else + return; // Plugin reloaders! + + if (unhookTask != null) + unhookTask.close(); protocolManager = null; statistisc = null; reporter = null; // Leaky ClassLoader begone! - CleanupStaticMembers cleanup = new CleanupStaticMembers(getClassLoader(), reporter); - cleanup.resetAll(); + if (updater == null || updater.getResult() != UpdateResult.SUCCESS) { + CleanupStaticMembers cleanup = new CleanupStaticMembers(getClassLoader(), reporter); + cleanup.resetAll(); + } } // Get the Bukkit logger first, before we try to create our own diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java index 54295cdb..78854be4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/BlockingHashMap.java @@ -19,15 +19,21 @@ package com.comphenix.protocol.concurrency; import java.util.Collection; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; -import com.google.common.collect.MapMaker; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.RemovalCause; +import com.google.common.cache.RemovalListener; +import com.google.common.cache.RemovalNotification; /** * 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. + * Values are stored as weak references, and will be automatically removed once they've all been dereferenced. *

* @author Kristian * @@ -35,19 +41,46 @@ import com.google.common.collect.MapMaker; * @param - type of the value. */ public class BlockingHashMap { - // Map of values + private final Cache backingCache; private final ConcurrentMap backingMap; - + // Map of locked objects private final ConcurrentMap locks; + /** + * Retrieve a cache loader that will always throw an exception. + * @return An invalid cache loader. + */ + public static CacheLoader newInvalidCacheLoader() { + return new CacheLoader() { + @Override + public TValue load(TKey key) throws Exception { + throw new IllegalStateException("Illegal use. Access the map directly instead."); + } + }; + } + /** * Initialize a new map. */ public BlockingHashMap() { - backingMap = new MapMaker().weakKeys().makeMap(); - locks = new MapMaker().weakKeys().makeMap(); + backingCache = CacheBuilder.newBuilder().weakValues().removalListener( + new RemovalListener() { + @Override + public void onRemoval(RemovalNotification entry) { + // Clean up locks too + if (entry.getCause() != RemovalCause.REPLACED) { + locks.remove(entry.getKey()); + } + } + }).build( + BlockingHashMap.newInvalidCacheLoader() + ); + backingMap = backingCache.asMap(); + + // Normal concurrent hash map + locks = new ConcurrentHashMap(); } /** @@ -94,34 +127,57 @@ public class BlockingHashMap { * @throws InterruptedException If the current thread got interrupted while waiting. */ public TValue get(TKey key, long timeout, TimeUnit unit) throws InterruptedException { + return get(key, timeout, unit, false); + } + + /** + * Waits until a value has been associated with the given key, and then retrieves that value. + *

+ * If timeout is zero, this method will return immediately if it can't find an socket injector. + * + * @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. + * @param ignoreInterrupted - TRUE if we should ignore the thread being interrupted, FALSE otherwise. + * @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, boolean ignoreInterrupted) throws InterruptedException { if (key == null) throw new IllegalArgumentException("key cannot be NULL."); if (unit == null) throw new IllegalArgumentException("Unit cannot be NULL."); + if (timeout < 0) + throw new IllegalArgumentException("Timeout cannot be less than zero."); TValue value = backingMap.get(key); // Only lock if no value is available - if (value == null) { + if (value == null && timeout > 0) { 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); - } else { - // Timeout elapsed - break; + try { + long remainingTime = stopTimeNS - System.nanoTime(); + + if (remainingTime > 0) { + TimeUnit.NANOSECONDS.timedWait(lock, remainingTime); + value = backingMap.get(key); + } else { + // Timeout elapsed + break; + } + } catch (InterruptedException e) { + // This is fairly dangerous - but we might HAVE to block the thread + if (!ignoreInterrupted) + throw e; } } } } - return value; } @@ -148,6 +204,29 @@ public class BlockingHashMap { } } + /** + * If and only if a key is not present in the map will it be associated with the given value. + * @param key - the key to associate. + * @param value - the value to associate. + * @return The previous value this key has been associated with. + */ + public TValue putIfAbsent(TKey key, TValue value) { + if (value == null) + throw new IllegalArgumentException("This map doesn't support NULL values."); + + final TValue previous = backingMap.putIfAbsent(key, value); + + // No need to unlock readers if we haven't changed anything + if (previous == null) { + final Object lock = getLock(key); + + synchronized (lock) { + lock.notifyAll(); + } + } + return previous; + } + public int size() { return backingMap.size(); } 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 93bc0d19..85cebfe7 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -96,6 +96,9 @@ public class PacketContainer implements Serializable { andThen(new Function() { @Override public Cloner apply(@Nullable BuilderParameters param) { + if (param == null) + throw new IllegalArgumentException("Cannot be NULL."); + return new FieldCloner(param.getAggregateCloner(), param.getInstanceProvider()) {{ // Use a default writer with no concept of cloning writer = new ObjectWriter(); 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 eea43aae..c5b8c0ac 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -56,6 +56,7 @@ import com.comphenix.protocol.injector.packet.PacketInjectorBuilder; import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; import com.comphenix.protocol.injector.player.PlayerInjectorBuilder; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy; import com.comphenix.protocol.injector.spigot.SpigotPacketInjector; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; @@ -133,8 +134,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok private AsyncFilterManager asyncFilterManager; // Valid server and client packets - private Set serverPackets; - private Set clientPackets; + private boolean knowsServerPackets; + private boolean knowsClientPackets; // Ensure that we're not performing too may injections private AtomicInteger phaseLoginCount = new AtomicInteger(0); @@ -214,8 +215,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Attempt to load the list of server and client packets try { - this.serverPackets = PacketRegistry.getServerPackets(); - this.clientPackets = PacketRegistry.getClientPackets(); + knowsServerPackets = PacketRegistry.getServerPackets() != null; + knowsClientPackets = PacketRegistry.getClientPackets() != null; } catch (FieldAccessException e) { reporter.reportWarning(this, "Cannot load server and client packet list.", e); } @@ -225,6 +226,13 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok } } + /** + * Initiate logic that is performed after the world has loaded. + */ + public void postWorldLoaded() { + playerInjection.postWorldLoaded(); + } + @Override public AsynchronousManager getAsynchronousManager() { return asyncFilterManager; @@ -275,15 +283,15 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Make sure this is possible playerInjection.checkListener(listener); } + if (hasSending) + incrementPhases(sending.getGamePhase()); + + // Handle receivers after senders if (hasReceiving) { verifyWhitelist(listener, receiving); recievedListeners.addListener(listener, receiving); enablePacketFilters(listener, ConnectionSide.CLIENT_SIDE, receiving.getWhitelist()); } - - // Increment phases too - if (hasSending) - incrementPhases(sending.getGamePhase()); if (hasReceiving) incrementPhases(receiving.getGamePhase()); @@ -466,7 +474,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok for (int packetID : packets) { // Only register server packets that are actually supported by Minecraft if (side.isForServer()) { - if (serverPackets != null && serverPackets.contains(packetID)) + // Note that we may update the packet list here + if (!knowsServerPackets || PacketRegistry.getServerPackets().contains(packetID)) playerInjection.addPacketHandler(packetID); else reporter.reportWarning(this, String.format( @@ -477,7 +486,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // As above, only for client packets if (side.isForClient() && packetInjector != null) { - if (clientPackets != null && clientPackets.contains(packetID)) + if (!knowsClientPackets || PacketRegistry.getClientPackets().contains(packetID)) packetInjector.addPacketHandler(packetID); else reporter.reportWarning(this, String.format( @@ -612,7 +621,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok */ public void initializePlayers(Player[] players) { for (Player player : players) - playerInjection.injectPlayer(player); + playerInjection.injectPlayer(player, ConflictStrategy.OVERRIDE); } /** @@ -672,7 +681,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok private void onPlayerJoin(PlayerJoinEvent event) { try { // This call will be ignored if no listeners are registered - playerInjection.injectPlayer(event.getPlayer()); + playerInjection.injectPlayer(event.getPlayer(), ConflictStrategy.OVERRIDE); } catch (Exception e) { reporter.reportDetailed(PacketFilterManager.this, "Unable to inject player.", e, event); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java index 73f4e587..918546f4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java @@ -47,8 +47,12 @@ public class PacketRegistry { private static Map packetToID; // Whether or not certain packets are sent by the client or the server - private static Set serverPackets; - private static Set clientPackets; + private static ImmutableSet serverPackets; + private static ImmutableSet clientPackets; + + // The underlying sets + private static Set serverPacketsRef; + private static Set clientPacketsRef; // New proxy values private static Map overwrittenPackets = new HashMap(); @@ -120,21 +124,21 @@ public class PacketRegistry { @SuppressWarnings("unchecked") private static void initializeSets() throws FieldAccessException { - if (serverPackets == null || clientPackets == null) { + if (serverPacketsRef == null || clientPacketsRef == null) { List sets = getPacketRegistry().getFieldListByType(Set.class); try { if (sets.size() > 1) { - serverPackets = (Set) FieldUtils.readStaticField(sets.get(0), true); - clientPackets = (Set) FieldUtils.readStaticField(sets.get(1), true); + serverPacketsRef = (Set) FieldUtils.readStaticField(sets.get(0), true); + clientPacketsRef = (Set) FieldUtils.readStaticField(sets.get(1), true); // Impossible - if (serverPackets == null || clientPackets == null) + if (serverPacketsRef == null || clientPacketsRef == null) throw new FieldAccessException("Packet sets are in an illegal state."); // NEVER allow callers to modify the underlying sets - serverPackets = ImmutableSet.copyOf(serverPackets); - clientPackets = ImmutableSet.copyOf(clientPackets); + serverPackets = ImmutableSet.copyOf(serverPacketsRef); + clientPackets = ImmutableSet.copyOf(clientPacketsRef); } else { throw new FieldAccessException("Cannot retrieve packet client/server sets."); @@ -143,6 +147,13 @@ public class PacketRegistry { } catch (IllegalAccessException e) { throw new FieldAccessException("Cannot access field.", e); } + + } else { + // Copy over again if it has changed + if (serverPacketsRef != null && serverPacketsRef.size() != serverPackets.size()) + serverPackets = ImmutableSet.copyOf(serverPacketsRef); + if (clientPacketsRef != null && clientPacketsRef.size() != clientPackets.size()) + clientPackets = ImmutableSet.copyOf(clientPacketsRef); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java index bd3274a8..391c321f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java @@ -23,13 +23,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - import org.bukkit.entity.Player; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.Factory; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.NoOp; +import com.comphenix.protocol.Packets; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; @@ -37,6 +39,8 @@ import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.MethodInfo; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.utility.MinecraftReflection; /** @@ -45,7 +49,15 @@ import com.comphenix.protocol.utility.MinecraftReflection; * @author Kristian */ class ProxyPacketInjector implements PacketInjector { - + /** + * Matches the readPacketData(DataInputStream) method in Packet. + */ + private static FuzzyMethodContract readPacket = FuzzyMethodContract.newBuilder(). + returnTypeVoid(). + parameterExactType(DataInputStream.class). + parameterCount(1). + build(); + // The "put" method that associates a packet ID with a packet class private static Method putMethod; private static Object intHashMap; @@ -59,11 +71,11 @@ class ProxyPacketInjector implements PacketInjector { // Allows us to determine the sender private PlayerInjectionHandler playerInjection; - // Allows us to look up read packet injectors - private Map readModifier; - // Class loader private ClassLoader classLoader; + + // Share callback filter + private CallbackFilter filter; public ProxyPacketInjector(ClassLoader classLoader, ListenerInvoker manager, PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException { @@ -72,7 +84,6 @@ class ProxyPacketInjector implements PacketInjector { this.manager = manager; this.playerInjection = playerInjection; this.reporter = reporter; - this.readModifier = new ConcurrentHashMap(); initialize(); } @@ -83,11 +94,9 @@ class ProxyPacketInjector implements PacketInjector { */ @Override public void undoCancel(Integer id, Object packet) { - ReadPacketModifier modifier = readModifier.get(id); - // See if this packet has been cancelled before - if (modifier != null && modifier.hasCancelled(packet)) { - modifier.removeOverride(packet); + if (ReadPacketModifier.hasCancelled(packet)) { + ReadPacketModifier.removeOverride(packet); } } @@ -131,22 +140,38 @@ class ProxyPacketInjector implements PacketInjector { throw new IllegalStateException("Packet ID " + packetID + " is not a valid packet ID in this version."); } // Check for previous injections - if (!MinecraftReflection.isMinecraftClass(old)) { + if (Factory.class.isAssignableFrom(old)) { throw new IllegalStateException("Packet " + packetID + " has already been injected."); } + if (filter == null) { + filter = new CallbackFilter() { + @Override + public int accept(Method method) { + // Skip methods defined in Object + if (method.getDeclaringClass().equals(Object.class)) + return 0; + else if (readPacket.isMatch(MethodInfo.fromMethod(method), null)) + return 1; + else + return 2; + } + }; + } + // Subclass the specific packet class ex.setSuperclass(old); - ex.setCallbackType(ReadPacketModifier.class); + ex.setCallbackFilter(filter); + ex.setCallbackTypes(new Class[] { NoOp.class, ReadPacketModifier.class, ReadPacketModifier.class }); ex.setClassLoader(classLoader); Class proxy = ex.createClass(); - // Create the proxy handler - ReadPacketModifier modifier = new ReadPacketModifier(packetID, this, reporter); - readModifier.put(packetID, modifier); - + // Create the proxy handlers + ReadPacketModifier modifierReadPacket = new ReadPacketModifier(packetID, this, reporter, true); + ReadPacketModifier modifierRest = new ReadPacketModifier(packetID, this, reporter, false); + // Add a static reference - Enhancer.registerStaticCallbacks(proxy, new Callback[] { modifier }); + Enhancer.registerStaticCallbacks(proxy, new Callback[] { NoOp.INSTANCE, modifierReadPacket, modifierRest }); try { // Override values @@ -182,7 +207,6 @@ class ProxyPacketInjector implements PacketInjector { putMethod.invoke(intHashMap, packetID, old); previous.remove(packetID); - readModifier.remove(packetID); registry.remove(proxy); overwritten.remove(packetID); return true; @@ -211,18 +235,22 @@ class ProxyPacketInjector implements PacketInjector { public PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) { try { Player client = playerInjection.getPlayerByConnection(input); - + // Never invoke a event if we don't know where it's from - if (client != null) + if (client != null) { return packetRecieved(packet, client); - else + } else { + // Hack #2 - Caused by our server socket injector + if (packet.getID() != Packets.Client.GET_INFO) + System.out.println("[ProtocolLib] Unknown origin " + input + " for packet " + packet.getID()); 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; - } + } } /** @@ -253,12 +281,4 @@ class ProxyPacketInjector implements PacketInjector { overwritten.clear(); previous.clear(); } - - /** - * Inform the current PlayerInjector that it should update the DataInputStream next. - * @param player - the player to update. - */ - public void scheduleDataInputRefresh(Player player) { - playerInjection.scheduleDataInputRefresh(player); - } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java index 00eacf9c..67ff3da2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java @@ -19,24 +19,17 @@ package com.comphenix.protocol.injector.packet; import java.io.DataInputStream; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collections; import java.util.Map; -import java.util.WeakHashMap; -import com.comphenix.protocol.Packets; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; +import com.google.common.collect.MapMaker; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; class ReadPacketModifier implements MethodInterceptor { - - @SuppressWarnings("rawtypes") - private static Class[] parameters = { DataInputStream.class }; - // A cancel marker private static final Object CANCEL_MARKER = new Object(); @@ -47,20 +40,24 @@ class ReadPacketModifier implements MethodInterceptor { // Report errors private ErrorReporter reporter; - // Whether or not a packet has been cancelled - private static Map override = Collections.synchronizedMap(new WeakHashMap()); + // If this is a read packet data method + private boolean isReadPacketDataMethod; - public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter) { + // Whether or not a packet has been cancelled + private static Map override = new MapMaker().weakKeys().makeMap(); + + public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter, boolean isReadPacketDataMethod) { this.packetID = packetID; this.packetInjector = packetInjector; this.reporter = reporter; + this.isReadPacketDataMethod = isReadPacketDataMethod; } /** * Remove any packet overrides. * @param packet - the packet to rever */ - public void removeOverride(Object packet) { + public static void removeOverride(Object packet) { override.remove(packet); } @@ -69,7 +66,7 @@ class ReadPacketModifier implements MethodInterceptor { * @param packet - the given packet. * @return Overriden object. */ - public Object getOverride(Object packet) { + public static Object getOverride(Object packet) { return override.get(packet); } @@ -78,23 +75,15 @@ class ReadPacketModifier implements MethodInterceptor { * @param packet - the packet to check. * @return TRUE if it has been cancelled, FALSE otherwise. */ - public boolean hasCancelled(Object packet) { + public static boolean hasCancelled(Object packet) { return getOverride(packet) == CANCEL_MARKER; } - + @Override public Object intercept(Object thisObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - - Object returnValue = null; - String methodName = method.getName(); - - // We always pass these down (otherwise, we'll end up with a infinite loop) - if (methodName.equals("hashCode") || methodName.equals("equals") || methodName.equals("toString")) { - return proxy.invokeSuper(thisObj, args); - } - // Atomic retrieval Object overridenObject = override.get(thisObj); + Object returnValue = null; if (overridenObject != null) { // This packet has been cancelled @@ -112,9 +101,7 @@ class ReadPacketModifier implements MethodInterceptor { } // Is this a readPacketData method? - if (returnValue == null && - Arrays.equals(method.getParameterTypes(), parameters)) { - + if (isReadPacketDataMethod) { try { // We need this in order to get the correct player DataInputStream input = (DataInputStream) args[0]; @@ -132,18 +119,12 @@ class ReadPacketModifier implements MethodInterceptor { } else if (!objectEquals(thisObj, result)) { override.put(thisObj, result); } - - // Update DataInputStream next time - if (!event.isCancelled() && packetID == Packets.Server.KEY_RESPONSE) { - packetInjector.scheduleDataInputRefresh(event.getPlayer()); - } } } catch (Throwable e) { // Minecraft cannot handle this error - reporter.reportDetailed(this, "Cannot handle clienet packet.", e, args[0]); + reporter.reportDetailed(this, "Cannot handle client packet.", e, args[0]); } } - return returnValue; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java index c6860ab6..4dd2fb9a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedServerConnection.java @@ -27,6 +27,7 @@ import net.sf.cglib.proxy.Factory; import org.bukkit.Server; import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.ObjectWriter; @@ -53,6 +54,9 @@ class InjectedServerConnection { // Used to inject net handlers private NetLoginInjector netLoginInjector; + // Inject server connections + private AbstractInputStreamLookup socketInjector; + private Server server; private ErrorReporter reporter; private boolean hasAttempted; @@ -60,11 +64,12 @@ class InjectedServerConnection { private Object minecraftServer = null; - public InjectedServerConnection(ErrorReporter reporter, Server server, NetLoginInjector netLoginInjector) { + public InjectedServerConnection(ErrorReporter reporter, AbstractInputStreamLookup socketInjector, Server server, NetLoginInjector netLoginInjector) { this.listFields = new ArrayList(); this.replacedLists = new ArrayList>(); this.reporter = reporter; this.server = server; + this.socketInjector = socketInjector; this.netLoginInjector = netLoginInjector; } @@ -126,6 +131,9 @@ class InjectedServerConnection { return; } + // Inject the server socket too + injectServerSocket(listenerThread); + // Just inject every list field we can get injectEveryListField(listenerThread, 1); hasSuccess = true; @@ -147,7 +155,8 @@ class InjectedServerConnection { listField = FuzzyReflection.fromClass(serverConnectionMethod.getReturnType(), true). getFieldByType("netServerHandlerList", List.class); if (dedicatedThreadField == null) { - List matches = FuzzyReflection.fromObject(serverConnection, true).getFieldListByType(Thread.class); + List matches = FuzzyReflection.fromObject(serverConnection, true). + getFieldListByType(Thread.class); // Verify the field count if (matches.size() != 1) @@ -158,8 +167,13 @@ class InjectedServerConnection { // Next, try to get the dedicated thread try { - if (dedicatedThreadField != null) - injectEveryListField(FieldUtils.readField(dedicatedThreadField, serverConnection, true), 1); + if (dedicatedThreadField != null) { + Object dedicatedThread = FieldUtils.readField(dedicatedThreadField, serverConnection, true); + + // Inject server socket and NetServerHandlers. + injectServerSocket(dedicatedThread); + injectEveryListField(dedicatedThread, 1); + } } catch (IllegalAccessException e) { reporter.reportWarning(this, "Unable to retrieve net handler thread.", e); } @@ -168,6 +182,10 @@ class InjectedServerConnection { hasSuccess = true; } + private void injectServerSocket(Object container) { + socketInjector.inject(container); + } + /** * Automatically inject into every List-compatible public or private field of the given object. * @param container - container object with the fields to inject. 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 f80bae7b..0fc27113 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 @@ -24,7 +24,8 @@ import org.bukkit.entity.Player; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.GamePhase; -import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler.ConflictStrategy; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; import com.comphenix.protocol.utility.MinecraftReflection; import com.google.common.collect.Maps; @@ -34,23 +35,22 @@ import com.google.common.collect.Maps; * @author Kristian */ class NetLoginInjector { - private ConcurrentMap injectedLogins = Maps.newConcurrentMap(); - + // Handles every hook private ProxyPlayerInjectionHandler injectionHandler; + + // Create temporary players + private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory(); + + // The current error reporter + private ErrorReporter reporter; private Server server; - // The current error rerporter - private ErrorReporter reporter; - - // Used to create fake players - private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); - - public NetLoginInjector(ErrorReporter reporter, ProxyPlayerInjectionHandler injectionHandler, Server server) { + public NetLoginInjector(ErrorReporter reporter, Server server, ProxyPlayerInjectionHandler injectionHandler) { this.reporter = reporter; - this.injectionHandler = injectionHandler; this.server = server; + this.injectionHandler = injectionHandler; } /** @@ -64,16 +64,19 @@ class NetLoginInjector { if (!injectionHandler.isInjectionNecessary(GamePhase.LOGIN)) return inserting; - Player fakePlayer = tempPlayerFactory.createTemporaryPlayer(server); - PlayerInjector injector = injectionHandler.injectPlayer(fakePlayer, inserting, GamePhase.LOGIN); - injector.updateOnLogin = true; + Player temporary = playerFactory.createTemporaryPlayer(server); + // Note that we bail out if there's an existing player injector + PlayerInjector injector = injectionHandler.injectPlayer( + temporary, inserting, ConflictStrategy.BAIL_OUT, GamePhase.LOGIN); - // Associate the injector too - InjectContainer container = (InjectContainer) fakePlayer; - container.setInjector(injector); - - // Save the login - injectedLogins.putIfAbsent(inserting, injector); + if (injector != null) { + // Update injector as well + TemporaryPlayerFactory.setInjectorInPlayer(temporary, injector); + injector.updateOnLogin = true; + + // Save the login + injectedLogins.putIfAbsent(inserting, injector); + } // NetServerInjector can never work (currently), so we don't need to replace the NetLoginHandler return inserting; @@ -81,7 +84,7 @@ class NetLoginInjector { } catch (Throwable e) { // Minecraft can't handle this, so we'll deal with it here reporter.reportDetailed(this, "Unable to hook " + - MinecraftReflection.getNetLoginHandlerName() + ".", e, inserting); + MinecraftReflection.getNetLoginHandlerName() + ".", e, inserting, injectionHandler); return inserting; } } @@ -108,15 +111,13 @@ class NetLoginInjector { // Hack to clean up other references newInjector = injectionHandler.getInjectorByNetworkHandler(injected.getNetworkManager()); + injectionHandler.uninjectPlayer(player); // Update NetworkManager - if (newInjector == null) { - injectionHandler.uninjectPlayer(player); - } else { - injectionHandler.uninjectPlayer(player, false); - - if (injected instanceof NetworkObjectInjector) + if (newInjector != null) { + if (injected instanceof NetworkObjectInjector) { newInjector.setNetworkManager(injected.getNetworkManager(), true); + } } } catch (Throwable e) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java index 855e2390..69acf343 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkObjectInjector.java @@ -39,7 +39,7 @@ import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; /** * Injection method that overrides the NetworkHandler itself, and its queue-method. @@ -54,7 +54,7 @@ public class NetworkObjectInjector extends PlayerInjector { private ClassLoader classLoader; // Shared callback filter - avoid creating a new class every time - private static CallbackFilter callbackFilter; + private volatile static CallbackFilter callbackFilter; // Temporary player factory private static volatile TemporaryPlayerFactory tempPlayerFactory; @@ -91,10 +91,8 @@ public class NetworkObjectInjector extends PlayerInjector { if (tempPlayerFactory == null) tempPlayerFactory = new TemporaryPlayerFactory(); - // Create and associate this fake player with this network injector - Player player = tempPlayerFactory.createTemporaryPlayer(server); - ((InjectContainer) player).setInjector(this); - return player; + // Create and associate the fake player with this network injector + return tempPlayerFactory.createTemporaryPlayer(server, this); } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java index f81be360..16218066 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkServerInjector.java @@ -20,15 +20,7 @@ package com.comphenix.protocol.injector.player; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; -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 net.sf.cglib.proxy.*; import org.bukkit.entity.Player; @@ -44,8 +36,8 @@ import com.comphenix.protocol.reflect.ObjectWriter; import com.comphenix.protocol.reflect.VolatileField; import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.reflect.instances.ExistingGenerator; +import com.comphenix.protocol.utility.MinecraftMethods; import com.comphenix.protocol.utility.MinecraftReflection; -import com.google.common.collect.Maps; /** * Represents a player hook into the NetServerHandler class. @@ -57,7 +49,6 @@ class NetworkServerInjector extends PlayerInjector { private volatile static CallbackFilter callbackFilter; private volatile static Field disconnectField; - private volatile static Method sendPacketMethod; private InjectedServerConnection serverInjection; // Determine if we're listening @@ -88,67 +79,6 @@ class NetworkServerInjector extends PlayerInjector { return sendingFilters.contains(packetID); } - @Override - public void initialize(Object injectionSource) throws IllegalAccessException { - super.initialize(injectionSource); - - // Get the send packet method! - if (hasInitialized) { - if (sendPacketMethod == null) { - try { - sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*"); - } catch (IllegalArgumentException e) { - Map netServer = getMethodList( - MinecraftReflection.getNetServerHandlerClass(), MinecraftReflection.getPacketClass()); - Map netHandler = getMethodList( - MinecraftReflection.getNetHandlerClass(), MinecraftReflection.getPacketClass()); - - // Remove every method in net handler from net server - for (String methodName : netHandler.keySet()) { - netServer.remove(methodName); - } - - // The remainder is the send packet method - if (netServer.size() == 1) { - Method[] methods = netServer.values().toArray(new Method[0]); - sendPacketMethod = methods[0]; - } else { - throw new IllegalArgumentException("Unable to find the sendPacket method in NetServerHandler/PlayerConnection."); - } - } - } - } - } - - /** - * Retrieve a method mapped list of every method with the given signature. - * @param source - class source. - * @param params - parameters. - * @return Method mapped list. - */ - private Map getMethodList(Class source, Class... params) { - return getMappedMethods( - FuzzyReflection.fromClass(source, true). - getMethodListByParameters(Void.TYPE, params) - ); - } - - /** - * Retrieve every method as a map over names. - *

- * Note that overloaded methods will only occur once in the resulting map. - * @param methods - every method. - * @return A map over every given method. - */ - private Map getMappedMethods(List methods) { - Map map = Maps.newHashMap(); - - for (Method method : methods) { - map.put(method.getName(), method); - } - return map; - } - @Override public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); @@ -156,7 +86,7 @@ class NetworkServerInjector extends PlayerInjector { if (serverDelegate != null) { try { // Note that invocation target exception is a wrapper for a checked exception - sendPacketMethod.invoke(serverDelegate, packet); + MinecraftMethods.getSendPacketMethod().invoke(serverDelegate, packet); } catch (IllegalArgumentException e) { throw e; @@ -180,6 +110,7 @@ class NetworkServerInjector extends PlayerInjector { return; if (!tryInjectManager()) { + Class serverHandlerClass = MinecraftReflection.getNetServerHandlerClass(); // Try to override the proxied object if (proxyServerField != null) { @@ -188,6 +119,8 @@ class NetworkServerInjector extends PlayerInjector { if (serverHandler == null) throw new RuntimeException("Cannot hook player: Inner proxy object is NULL."); + else + serverHandlerClass = serverHandler.getClass(); // Try again if (tryInjectManager()) { @@ -198,7 +131,7 @@ class NetworkServerInjector extends PlayerInjector { throw new RuntimeException( "Cannot hook player: Unable to find a valid constructor for the " - + MinecraftReflection.getNetServerHandlerClass().getName() + " object."); + + serverHandlerClass.getName() + " object."); } } @@ -226,14 +159,16 @@ class NetworkServerInjector extends PlayerInjector { }; }; Callback noOpCallback = NoOp.INSTANCE; - + // Share callback filter - that way, we avoid generating a new class for // every logged in player. if (callbackFilter == null) { + final Method sendPacket = MinecraftMethods.getSendPacketMethod(); + callbackFilter = new CallbackFilter() { @Override public int accept(Method method) { - if (method.equals(sendPacketMethod)) + if (method.equals(sendPacket)) return 0; else return 1; 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 a1fe1b44..8c59c15a 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 @@ -4,8 +4,6 @@ import java.io.DataInputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.util.Set; -import java.util.concurrent.TimeUnit; - import org.bukkit.entity.Player; import com.comphenix.protocol.events.PacketContainer; @@ -14,6 +12,23 @@ import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; public interface PlayerInjectionHandler { + /** + * How to handle a previously existing player injection. + * + * @author Kristian + */ + public enum ConflictStrategy { + /** + * Override it. + */ + OVERRIDE, + + /** + * Immediately exit. + */ + BAIL_OUT; + } + /** * Retrieves how the server packets are read. * @return Injection method for reading server packets. @@ -61,23 +76,14 @@ public interface PlayerInjectionHandler { public abstract Player getPlayerByConnection(DataInputStream inputStream) throws InterruptedException; - /** - * 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 abstract Player getPlayerByConnection(DataInputStream inputStream, long playerTimeout, TimeUnit unit) throws InterruptedException; - /** * Initialize a player hook, allowing us to read server packets. *

* This call will be ignored if there's no listener that can receive the given events. * @param player - player to hook. + * @param strategy - how to handle injection conflicts. */ - public abstract void injectPlayer(Player player); + public abstract void injectPlayer(Player player, ConflictStrategy strategy); /** * Invoke special routines for handling disconnect before a player is uninjected. @@ -149,8 +155,7 @@ public interface PlayerInjectionHandler { public abstract void close(); /** - * Inform the current PlayerInjector that it should update the DataInputStream next. - * @param player - the player to update. + * Perform any action that must be delayed until the world(s) has loaded. */ - public abstract void scheduleDataInputRefresh(Player player); + public abstract void postWorldLoaded(); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java index 8f84cdd0..524bd40a 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjector.java @@ -24,7 +24,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Socket; import java.net.SocketAddress; - import net.sf.cglib.proxy.Factory; import org.bukkit.entity.Player; @@ -38,13 +37,14 @@ import com.comphenix.protocol.injector.BukkitUnwrapper; import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.comphenix.protocol.injector.server.SocketInjector; import com.comphenix.protocol.reflect.FieldUtils; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.VolatileField; import com.comphenix.protocol.utility.MinecraftReflection; -abstract class PlayerInjector { +abstract class PlayerInjector implements SocketInjector { // Net login handler stuff private static Field netLoginNetworkField; @@ -60,6 +60,7 @@ abstract class PlayerInjector { protected static Field networkManagerField; protected static Field netHandlerField; protected static Field socketField; + protected static Field socketAddressField; private static Field inputField; private static Field entityPlayerField; @@ -87,8 +88,9 @@ abstract class PlayerInjector { protected Object serverHandler; protected Object netHandler; - // Current socket + // Current socket and address protected Socket socket; + protected SocketAddress socketAddress; // The packet manager and filters protected ListenerInvoker invoker; @@ -99,9 +101,6 @@ abstract class PlayerInjector { // Handle errors protected ErrorReporter reporter; - // Scheduled action on the next packet event - protected Runnable scheduledAction; - // Whether or not the injector has been cleaned private boolean clean; @@ -249,10 +248,12 @@ abstract class PlayerInjector { * @return The associated socket. * @throws IllegalAccessException If we're unable to read the socket field. */ + @Override public Socket getSocket() throws IllegalAccessException { try { if (socketField == null) - socketField = FuzzyReflection.fromObject(networkManager, true).getFieldListByType(Socket.class).get(0); + socketField = FuzzyReflection.fromObject(networkManager, true). + getFieldListByType(Socket.class).get(0); if (socket == null) socket = (Socket) FieldUtils.readField(socketField, networkManager, true); return socket; @@ -263,18 +264,23 @@ abstract class PlayerInjector { } /** - * Retrieve the associated address of this player. - * @return The associated address. - * @throws IllegalAccessException If we're unable to read the socket field. + * Retrieve the associated remote address of a player. + * @return The associated remote address.. + * @throws IllegalAccessException If we're unable to read the socket address field. */ + @Override public SocketAddress getAddress() throws IllegalAccessException { - Socket socket = getSocket(); - - // Guard against NULL - if (socket != null) - return socket.getRemoteSocketAddress(); - else - return null; + try { + if (socketAddressField == null) + socketAddressField = FuzzyReflection.fromObject(networkManager, true). + getFieldListByType(SocketAddress.class).get(0); + if (socketAddress == null) + socketAddress = (SocketAddress) FieldUtils.readField(socketAddressField, networkManager, true); + return socketAddress; + + } catch (IndexOutOfBoundsException e) { + throw new IllegalAccessException("Unable to read the socket address field."); + } } /** @@ -282,6 +288,7 @@ abstract class PlayerInjector { * @param message - the message to display. * @throws InvocationTargetException If disconnection failed. */ + @Override public void disconnect(String message) throws InvocationTargetException { // Get a non-null handler boolean usingNetServer = serverHandler != null; @@ -450,6 +457,7 @@ abstract class PlayerInjector { * @param filtered - whether or not the packet will be filtered by our listeners. * @param InvocationTargetException If an error occured when sending the packet. */ + @Override public abstract void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException; /** @@ -517,12 +525,7 @@ abstract class PlayerInjector { Integer id = invoker.getPacketID(packet); Player currentPlayer = player; - // Hack #1: Handle a single scheduled action - if (scheduledAction != null) { - scheduledAction.run(); - scheduledAction = null; - } - // Hack #2 + // Hack #1 if (updateOnLogin) { if (id == Packets.Server.LOGIN) { try { @@ -593,17 +596,10 @@ abstract class PlayerInjector { } } - /** - * Schedule an action to occur on the next sent packet. - * @param action - action to execute. - */ - public void scheduleAction(Runnable action) { - scheduledAction = action; - } - /** * Retrieve the hooked player. */ + @Override public Player getPlayer() { return player; } @@ -630,6 +626,7 @@ abstract class PlayerInjector { * Retrieve the hooked player object OR the more up-to-date player instance. * @return The hooked player, or a more up-to-date instance. */ + @Override public Player getUpdatedPlayer() { if (updatedPlayer != null) return updatedPlayer; @@ -637,6 +634,11 @@ abstract class PlayerInjector { return player; } + @Override + public void transferState(SocketInjector delegate) { + // Do nothing + } + /** * Set the real Bukkit player that we will use. * @param updatedPlayer - the real Bukkit player. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java index 053fe0ef..35020261 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java @@ -20,12 +20,13 @@ package com.comphenix.protocol.injector.player; import java.io.DataInputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; -import java.net.Socket; import java.net.SocketAddress; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import net.sf.cglib.proxy.Factory; + import org.bukkit.Server; import org.bukkit.entity.Player; @@ -40,8 +41,14 @@ import com.comphenix.protocol.injector.GamePhase; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PlayerLoggedOutException; import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; -import com.comphenix.protocol.injector.player.TemporaryPlayerFactory.InjectContainer; +import com.comphenix.protocol.injector.server.AbstractInputStreamLookup; +import com.comphenix.protocol.injector.server.InputStreamLookupBuilder; +import com.comphenix.protocol.injector.server.SocketInjector; +import com.comphenix.protocol.utility.MinecraftReflection; + import com.google.common.base.Predicate; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.Maps; /** @@ -50,26 +57,26 @@ import com.google.common.collect.Maps; * @author Kristian */ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { - /** - * The maximum number of milliseconds to wait until a player can be looked up by connection. - */ - private static final long TIMEOUT_PLAYER_LOOKUP = 2000; // ms - // Server connection injection private InjectedServerConnection serverInjection; + // Server socket injection + private AbstractInputStreamLookup inputStreamLookup; + // NetLogin injector private NetLoginInjector netLoginInjector; // The last successful player hook private PlayerInjector lastSuccessfulHook; - // Player injection - private Map addressLookup = Maps.newConcurrentMap(); - private Map playerInjection = Maps.newConcurrentMap(); + // Dummy injection + private Cache dummyInjectors = + CacheBuilder.newBuilder(). + expireAfterWrite(30, TimeUnit.SECONDS). + build(BlockingHashMap.newInvalidCacheLoader()); - // Lookup player by connection - private BlockingHashMap dataInputLookup = BlockingHashMap.create(); + // Player injection + private Map playerInjection = Maps.newConcurrentMap(); // Player injection types private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; @@ -96,18 +103,34 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { // Used to filter injection attempts private Predicate injectionFilter; - public ProxyPlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, - ListenerInvoker invoker, Set packetListeners, Server server) { + public ProxyPlayerInjectionHandler( + ClassLoader classLoader, ErrorReporter reporter, Predicate injectionFilter, + ListenerInvoker invoker, Set packetListeners, Server server) { this.classLoader = classLoader; this.reporter = reporter; this.invoker = invoker; this.injectionFilter = injectionFilter; this.packetListeners = packetListeners; - this.netLoginInjector = new NetLoginInjector(reporter, this, server); - this.serverInjection = new InjectedServerConnection(reporter, server, netLoginInjector); + + this.inputStreamLookup = InputStreamLookupBuilder.newBuilder(). + server(server). + reporter(reporter). + build(); + + // Create net login injectors and the server connection injector + this.netLoginInjector = new NetLoginInjector(reporter, server, this); + this.serverInjection = new InjectedServerConnection(reporter, inputStreamLookup, server, netLoginInjector); serverInjection.injectList(); } + + @Override + public void postWorldLoaded() { + // This will actually create a socket and a seperate thread ... + if (inputStreamLookup != null) { + inputStreamLookup.postWorldLoaded(); + } + } /** * Retrieves how the server packets are read. @@ -202,31 +225,16 @@ class ProxyPlayerInjectionHandler implements 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. + * @return The player we found. */ @Override - 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. - */ - @Override - public Player getPlayerByConnection(DataInputStream inputStream, long playerTimeout, TimeUnit unit) throws InterruptedException { + public Player getPlayerByConnection(DataInputStream inputStream) { // Wait until the connection owner has been established - PlayerInjector injector = dataInputLookup.get(inputStream, playerTimeout, unit); + SocketInjector injector = inputStreamLookup.waitSocketInjector(inputStream); if (injector != null) { return injector.getPlayer(); } else { - reporter.reportWarning(this, "Unable to find stream: " + inputStream); return null; } } @@ -245,12 +253,13 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { *

* This call will be ignored if there's no listener that can receive the given events. * @param player - player to hook. + * @param strategy - how to handle previous player injections. */ @Override - public void injectPlayer(Player player) { + public void injectPlayer(Player player, ConflictStrategy strategy) { // Inject using the player instance itself if (isInjectionNecessary(GamePhase.PLAYING)) { - injectPlayer(player, player, GamePhase.PLAYING); + injectPlayer(player, player, strategy, GamePhase.PLAYING); } } @@ -273,16 +282,22 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { * @param phase - the current game phase. * @return The resulting player injector, or NULL if the injection failed. */ - PlayerInjector injectPlayer(Player player, Object injectionPoint, GamePhase phase) { + PlayerInjector injectPlayer(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { + if (player == null) + throw new IllegalArgumentException("Player cannot be NULL."); + if (injectionPoint == null) + throw new IllegalArgumentException("injectionPoint cannot be NULL."); + if (phase == null) + throw new IllegalArgumentException("phase cannot be NULL."); + // Unfortunately, due to NetLoginHandler, multiple threads may potentially call this method. synchronized (player) { - return injectPlayerInternal(player, injectionPoint, phase); + return injectPlayerInternal(player, injectionPoint, stategy, phase); } } // Unsafe variant of the above - private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, GamePhase phase) { - + private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, ConflictStrategy stategy, GamePhase phase) { PlayerInjector injector = playerInjection.get(player); PlayerInjectHooks tempHook = getPlayerHook(phase); PlayerInjectHooks permanentHook = tempHook; @@ -293,7 +308,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { boolean invalidInjector = injector != null ? !injector.canInject(phase) : true; // Don't inject if the class has closed - if (!hasClosed && player != null && (tempHook != getInjectorType(injector) || invalidInjector)) { + if (!hasClosed && (tempHook != getInjectorType(injector) || invalidInjector)) { while (tempHook != PlayerInjectHooks.NONE) { // Whether or not the current hook method failed completely boolean hookFailed = false; @@ -308,25 +323,24 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { if (injector.canInject(phase)) { injector.initialize(injectionPoint); - DataInputStream inputStream = injector.getInputStream(false); + // Get socket and socket injector + SocketAddress address = injector.getAddress(); + SocketInjector previous = inputStreamLookup.peekSocketInjector(address); - Socket socket = injector.getSocket(); - SocketAddress address = socket != null ? socket.getRemoteSocketAddress() : null; - - // Guard against NPE here too - PlayerInjector previous = address != null ? addressLookup.get(address) : null; - // Close any previously associated hooks before we proceed - if (previous != null) { - uninjectPlayer(previous.getPlayer(), false, true); + if (previous != null && !(player instanceof Factory)) { + switch (stategy) { + case OVERRIDE: + uninjectPlayer(previous.getPlayer(), true); + break; + case BAIL_OUT: + return null; + } } - injector.injectManager(); - if (inputStream != null) - dataInputLookup.put(inputStream, injector); - if (address != null) - addressLookup.put(address, injector); + // Save injector + inputStreamLookup.setSocketInjector(address, injector); break; } @@ -373,7 +387,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { return injector; } - + private void cleanupHook(PlayerInjector injector) { // Clean up as much as possible try { @@ -404,33 +418,21 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { */ @Override public boolean uninjectPlayer(Player player) { - return uninjectPlayer(player, true, false); + return uninjectPlayer(player, false); } /** * Unregisters the given player. * @param player - player to unregister. - * @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address. - * @return TRUE if a player has been uninjected, FALSE otherwise. - */ - public boolean uninjectPlayer(Player player, boolean removeAuxiliary) { - return uninjectPlayer(player, removeAuxiliary, false); - } - - /** - * Unregisters the given player. - * @param player - player to unregister. - * @param removeAuxiliary - TRUE to remove auxiliary information, such as input stream and address. * @param prepareNextHook - whether or not we need to fix any lingering hooks. * @return TRUE if a player has been uninjected, FALSE otherwise. */ - private boolean uninjectPlayer(Player player, boolean removeAuxiliary, boolean prepareNextHook) { + private boolean uninjectPlayer(Player player, boolean prepareNextHook) { if (!hasClosed && player != null) { PlayerInjector injector = playerInjection.remove(player); if (injector != null) { - InetSocketAddress address = player.getAddress(); injector.cleanupAll(); // Remove the "hooked" network manager in our instance as well @@ -446,12 +448,6 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { } } - // Clean up - if (removeAuxiliary) { - // Note that the dataInputLookup will clean itself - if (address != null) - addressLookup.remove(address); - } return true; } } @@ -471,11 +467,11 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { @Override public boolean uninjectPlayer(InetSocketAddress address) { if (!hasClosed && address != null) { - PlayerInjector injector = addressLookup.get(address); + SocketInjector injector = inputStreamLookup.peekSocketInjector(address); // Clean up if (injector != null) - uninjectPlayer(injector.getPlayer(), false, true); + uninjectPlayer(injector.getPlayer(), true); return true; } @@ -491,7 +487,7 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { */ @Override public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { - PlayerInjector injector = getInjector(reciever); + SocketInjector injector = getInjector(reciever); // Send the packet, or drop it completely if (injector != null) { @@ -513,7 +509,6 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { */ @Override public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { - PlayerInjector injector = getInjector(player); // Process the given packet, or simply give up @@ -536,28 +531,49 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { if (injector == null) { // Try getting it from the player itself - if (player instanceof InjectContainer) - return ((InjectContainer) player).getInjector(); + SocketAddress address = player.getAddress(); + // Look that up without blocking + SocketInjector result = inputStreamLookup.peekSocketInjector(address); + + // Ensure that it is non-null and a player injector + if (result instanceof PlayerInjector) + return (PlayerInjector) result; else - return searchAddressLookup(player); + // Make a dummy injector them + return createDummyInjector(player); + } else { return injector; } } /** - * Find an injector by looking through the address map. - * @param player - player to find. - * @return The injector, or NULL if not found. + * Construct a simple dummy injector incase none has been constructed. + * @param player - the CraftPlayer to construct for. + * @return A dummy injector, or NULL if the given player is not a CraftPlayer. */ - private PlayerInjector searchAddressLookup(Player player) { - // See if we can find it anywhere - for (PlayerInjector injector : addressLookup.values()) { - if (player.equals(injector.getUpdatedPlayer())) { - return injector; - } + private PlayerInjector createDummyInjector(Player player) { + if (!MinecraftReflection.getCraftPlayerClass().isAssignableFrom(player.getClass())) { + // No - this is not safe + return null; + } + + try { + PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); + dummyInjector.initializePlayer(player); + + // This probably means the player has disconnected + if (dummyInjector.getSocket() == null) { + return null; + } + + inputStreamLookup.setSocketInjector(dummyInjector.getAddress(), dummyInjector); + dummyInjectors.asMap().put(player, dummyInjector); + return dummyInjector; + + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access fields.", e); } - return null; } /** @@ -640,35 +656,18 @@ class ProxyPlayerInjectionHandler implements PlayerInjectionHandler { } // Remove server handler + if (inputStreamLookup != null) + inputStreamLookup.cleanupAll(); if (serverInjection != null) serverInjection.cleanupAll(); if (netLoginInjector != null) netLoginInjector.cleanupAll(); + inputStreamLookup = null; serverInjection = null; netLoginInjector = null; hasClosed = true; playerInjection.clear(); - addressLookup.clear(); invoker = null; } - - /** - * Inform the current PlayerInjector that it should update the DataInputStream next. - * @param player - the player to update. - */ - @Override - public void scheduleDataInputRefresh(Player player) { - final PlayerInjector injector = getInjector(player); - - // Update the DataInputStream - if (injector != null) { - injector.scheduleAction(new Runnable() { - @Override - public void run() { - dataInputLookup.put(injector.getInputStream(false), injector); - } - }); - } - } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java new file mode 100644 index 00000000..2ed52ae4 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/AbstractInputStreamLookup.java @@ -0,0 +1,119 @@ +package com.comphenix.protocol.injector.server; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketAddress; +import org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; + +public abstract class AbstractInputStreamLookup { + // Used to access the inner input stream of a filtered input stream + private static Field filteredInputField; + + // Error reporter + protected final ErrorReporter reporter; + + // Reference to the server itself + protected final Server server; + + protected AbstractInputStreamLookup(ErrorReporter reporter, Server server) { + this.reporter = reporter; + this.server = server; + } + + /** + * Retrieve the underlying input stream that is associated with a given filter input stream. + * @param filtered - the filter input stream. + * @return The underlying input stream that is being filtered. + * @throws FieldAccessException Unable to access input stream. + */ + protected static InputStream getInputStream(FilterInputStream filtered) { + if (filteredInputField == null) + filteredInputField = FuzzyReflection.fromClass(FilterInputStream.class, true). + getFieldByType("in", InputStream.class); + + InputStream current = filtered; + + try { + // Iterate until we find the real input stream + while (current instanceof FilterInputStream) { + current = (InputStream) FieldUtils.readField(filteredInputField, current, true); + } + return current; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access filtered input field.", e); + } + } + + /** + * Inject the given server thread or dedicated connection. + * @param container - class that contains a ServerSocket field. + */ + public abstract void inject(Object container); + + /** + * Invoked when the world has loaded. + */ + public abstract void postWorldLoaded(); + + /** + * Retrieve the associated socket injector for a player. + * @param input - the indentifying filtered input stream. + * @return The socket injector we have associated with this player. + */ + public abstract SocketInjector waitSocketInjector(InputStream input); + + /** + * Retrieve an injector by its socket. + * @param socket - the socket. + * @return The socket injector. + */ + public abstract SocketInjector waitSocketInjector(Socket socket); + + /** + * Retrieve a injector by its address. + * @param address - the address of the socket. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector waitSocketInjector(SocketAddress address); + + /** + * Attempt to get a socket injector without blocking the thread. + * @param address - the address to lookup. + * @return The socket injector, or NULL if not found. + */ + public abstract SocketInjector peekSocketInjector(SocketAddress address); + + /** + * Associate a given socket address to the provided socket injector. + * @param address - the socket address to associate. + * @param injector - the injector. + */ + public abstract void setSocketInjector(SocketAddress address, SocketInjector injector); + + /** + * If a player can hold a reference to its parent injector, this method will update that reference. + * @param previous - the previous injector. + * @param current - the new injector. + */ + protected void onPreviousSocketOverwritten(SocketInjector previous, SocketInjector current) { + Player player = previous.getPlayer(); + + // Default implementation + if (player instanceof InjectorContainer) { + TemporaryPlayerFactory.setInjectorInPlayer(player, current); + } + } + + /** + * Invoked when the injection should be undone. + */ + public abstract void cleanupAll(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectorContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectorContainer.java new file mode 100644 index 00000000..7a642a09 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InjectorContainer.java @@ -0,0 +1,21 @@ +package com.comphenix.protocol.injector.server; + +/** + * Able to store a socket injector. + *

+ * A necessary hack. + * @author Kristian + */ +class InjectorContainer { + private volatile SocketInjector injector; + + public SocketInjector getInjector() { + return injector; + } + + public void setInjector(SocketInjector injector) { + if (injector == null) + throw new IllegalArgumentException("Injector cannot be NULL."); + this.injector = injector; + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java new file mode 100644 index 00000000..29989e5d --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamLookupBuilder.java @@ -0,0 +1,47 @@ +package com.comphenix.protocol.injector.server; + +import org.bukkit.Server; + +import com.comphenix.protocol.error.ErrorReporter; + +/** + * Constructs the appropriate input stream lookup for the current JVM and architecture. + * + * @author Kristian + */ +public class InputStreamLookupBuilder { + public static InputStreamLookupBuilder newBuilder() { + return new InputStreamLookupBuilder(); + } + + protected InputStreamLookupBuilder() { + // Use the static method. + } + + private Server server; + private ErrorReporter reporter; + + /** + * Set the server instance to use. + * @param server - server instance. + * @return The current builder, for chaining. + */ + public InputStreamLookupBuilder server(Server server) { + this.server = server; + return this; + } + + /** + * Set the error reporter to pass on to the lookup. + * @param reporter - the error reporter. + * @return The current builder, for chaining. + */ + public InputStreamLookupBuilder reporter(ErrorReporter reporter) { + this.reporter = reporter; + return this; + } + + public AbstractInputStreamLookup build() { + return new InputStreamReflectLookup(reporter, server); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java new file mode 100644 index 00000000..f374e9e9 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/InputStreamReflectLookup.java @@ -0,0 +1,164 @@ +package com.comphenix.protocol.injector.server; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.Socket; +import java.net.SocketAddress; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import org.bukkit.Server; + +import com.comphenix.protocol.concurrency.BlockingHashMap; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.collect.MapMaker; + +class InputStreamReflectLookup extends AbstractInputStreamLookup { + // The default lookup timeout + private static final long DEFAULT_TIMEOUT = 2000; // ms + + // Using weak keys and values ensures that we will not hold up garbage collection + protected BlockingHashMap addressLookup = new BlockingHashMap(); + protected ConcurrentMap inputLookup = new MapMaker().weakValues().makeMap(); + + // The timeout + private final long injectorTimeout; + + public InputStreamReflectLookup(ErrorReporter reporter, Server server) { + this(reporter, server, DEFAULT_TIMEOUT); + } + + /** + * Initialize a reflect lookup with a given default injector timeout. + *

+ * This timeout defines the maximum amount of time to wait until an injector has been discovered. + * @param reporter - the error reporter. + * @param server - the current Bukkit server. + * @param injectorTimeout - the injector timeout. + */ + public InputStreamReflectLookup(ErrorReporter reporter, Server server, long injectorTimeout) { + super(reporter, server); + this.injectorTimeout = injectorTimeout; + } + + @Override + public void inject(Object container) { + // Do nothing + } + + @Override + public void postWorldLoaded() { + // Nothing again + } + + @Override + public SocketInjector peekSocketInjector(SocketAddress address) { + try { + return addressLookup.get(address, 0, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Whatever + return null; + } + } + + @Override + public SocketInjector waitSocketInjector(SocketAddress address) { + try { + // Note that we actually SWALLOW interrupts here - this is because Minecraft uses interrupts to + // periodically wake up waiting readers and writers. We have to wait for the dedicated server thread + // to catch up, so we'll swallow these interrupts. + // + // TODO: Consider if we should raise the thread priority of the dedicated server listener thread. + return addressLookup.get(address, injectorTimeout, TimeUnit.MILLISECONDS, true); + } catch (InterruptedException e) { + // This cannot be! + throw new IllegalStateException("Impossible exception occured!", e); + } + } + + @Override + public SocketInjector waitSocketInjector(Socket socket) { + return waitSocketInjector(socket.getRemoteSocketAddress()); + } + + @Override + public SocketInjector waitSocketInjector(InputStream input) { + try { + SocketAddress address = waitSocketAddress(input); + + // Guard against NPE + if (address != null) + return waitSocketInjector(address); + else + return null; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot find or access socket field for " + input, e); + } + } + + /** + * Use reflection to get the underlying socket address from an input stream. + * @param stream - the socket stream to lookup. + * @return The underlying socket address, or NULL if not found. + * @throws IllegalAccessException Unable to access socket field. + */ + private SocketAddress waitSocketAddress(InputStream stream) throws IllegalAccessException { + // Extra check, just in case + if (stream instanceof FilterInputStream) + return waitSocketAddress(getInputStream((FilterInputStream) stream)); + + SocketAddress result = inputLookup.get(stream); + + if (result == null) { + Socket socket = lookupSocket(stream); + + // Save it + result = socket.getRemoteSocketAddress(); + inputLookup.put(stream, result); + } + return result; + } + + @Override + public void setSocketInjector(SocketAddress address, SocketInjector injector) { + if (address == null) + throw new IllegalArgumentException("address cannot be NULL"); + if (injector == null) + throw new IllegalArgumentException("injector cannot be NULL."); + + SocketInjector previous = addressLookup.put(address, injector); + + // Any previous temporary players will also be associated + if (previous != null) { + // Update the reference to any previous injector + onPreviousSocketOverwritten(previous, injector); + } + } + + @Override + public void cleanupAll() { + // Do nothing + } + + /** + * Lookup the underlying socket of a stream through reflection. + * @param stream - the socket stream. + * @return The underlying socket. + * @throws IllegalAccessException If reflection failed. + */ + private static Socket lookupSocket(InputStream stream) throws IllegalAccessException { + if (stream instanceof FilterInputStream) { + return lookupSocket(getInputStream((FilterInputStream) stream)); + } else { + // Just do it + Field socketField = FuzzyReflection.fromObject(stream, true). + getFieldByType("socket", Socket.class); + + return (Socket) FieldUtils.readField(socketField, stream, true); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java new file mode 100644 index 00000000..6407d320 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/SocketInjector.java @@ -0,0 +1,61 @@ +package com.comphenix.protocol.injector.server; + +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketAddress; + +import org.bukkit.entity.Player; + +/** + * Represents an injector that only gives access to a player's socket. + * + * @author Kristian + */ +public interface SocketInjector { + /** + * Retrieve the associated socket of this player. + * @return The associated socket. + * @throws IllegalAccessException If we're unable to read the socket field. + */ + public abstract Socket getSocket() throws IllegalAccessException; + + /** + * Retrieve the associated address of this player. + * @return The associated address. + * @throws IllegalAccessException If we're unable to read the socket field. + */ + public abstract SocketAddress getAddress() throws IllegalAccessException; + + /** + * Attempt to disconnect the current client. + * @param message - the message to display. + * @throws InvocationTargetException If disconnection failed. + */ + public abstract void disconnect(String message) throws InvocationTargetException; + + /** + * Send a packet to the client. + * @param packet - server packet to send. + * @param filtered - whether or not the packet will be filtered by our listeners. + * @param InvocationTargetException If an error occured when sending the packet. + */ + public abstract void sendServerPacket(Object packet, boolean filtered) + throws InvocationTargetException; + + /** + * Retrieve the hooked player. + */ + public abstract Player getPlayer(); + + /** + * Retrieve the hooked player object OR the more up-to-date player instance. + * @return The hooked player, or a more up-to-date instance. + */ + public abstract Player getUpdatedPlayer(); + + /** + * Invoked when a delegated socket injector transfers the state of one injector to the next. + * @param delegate - the new injector. + */ + public abstract void transferState(SocketInjector delegate); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java similarity index 76% rename from ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java index 67336df5..91d1fed6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/server/TemporaryPlayerFactory.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol.injector.player; +package com.comphenix.protocol.injector.server; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -36,26 +36,7 @@ import com.comphenix.protocol.reflect.FieldAccessException; /** * Create fake player instances that represents pre-authenticated clients. */ -class TemporaryPlayerFactory { - - /** - * Able to store a PlayerInjector. - *

- * A necessary hack. - * @author Kristian - */ - public static class InjectContainer { - private PlayerInjector injector; - - public PlayerInjector getInjector() { - return injector; - } - - public void setInjector(PlayerInjector injector) { - this.injector = injector; - } - } - +public class TemporaryPlayerFactory { // Helpful constructors private final PacketConstructor chatPacket; @@ -66,6 +47,27 @@ class TemporaryPlayerFactory { chatPacket = PacketConstructor.DEFAULT.withPacket(3, new Object[] { "DEMO" }); } + /** + * Retrieve the injector from a given player if it contains one. + * @param player - the player that may contain a reference to a player injector. + * @return The referenced player injector, or NULL if none can be found. + */ + public static SocketInjector getInjectorFromPlayer(Player player) { + if (player instanceof InjectorContainer) { + return ((InjectorContainer) player).getInjector(); + } + return null; + } + + /** + * Set the player injector, if possible. + * @param player - the player to update. + * @param injector - the injector to store. + */ + public static void setInjectorInPlayer(Player player, SocketInjector injector) { + ((InjectorContainer) player).setInjector(injector); + } + /** * Construct a temporary player that supports a subset of every player command. *

@@ -80,7 +82,7 @@ class TemporaryPlayerFactory { *

  • kickPlayer(String)
  • * *

    - * Note that the player a player has not been assigned a name yet, and thus cannot be + * Note that a temporary player has not yet been assigned a name, and thus cannot be * uniquely identified. Use the address instead. * @param injector - the player injector used. * @param server - the current server. @@ -94,7 +96,7 @@ class TemporaryPlayerFactory { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { String methodName = method.getName(); - PlayerInjector injector = ((InjectContainer) obj).getInjector(); + SocketInjector injector = ((InjectorContainer) obj).getInjector(); if (injector == null) throw new IllegalStateException("Unable to find injector."); @@ -147,7 +149,7 @@ class TemporaryPlayerFactory { public int accept(Method method) { // Do not override the object method or the superclass methods if (method.getDeclaringClass().equals(Object.class) || - method.getDeclaringClass().equals(InjectContainer.class)) + method.getDeclaringClass().equals(InjectorContainer.class)) return 0; else return 1; @@ -157,7 +159,7 @@ class TemporaryPlayerFactory { // CGLib is amazing Enhancer ex = new Enhancer(); - ex.setSuperclass(InjectContainer.class); + ex.setSuperclass(InjectorContainer.class); ex.setInterfaces(new Class[] { Player.class }); ex.setCallbacks(new Callback[] { NoOp.INSTANCE, implementation }); ex.setCallbackFilter(callbackFilter); @@ -165,6 +167,19 @@ class TemporaryPlayerFactory { return (Player) ex.create(); } + /** + * Construct a temporary player with the given associated socket injector. + * @param server - the parent server. + * @param injector - the referenced socket injector. + * @return The temporary player. + */ + public Player createTemporaryPlayer(Server server, SocketInjector injector) { + Player temporary = createTemporaryPlayer(server); + + ((InjectorContainer) temporary).setInjector(injector); + return temporary; + } + /** * Send a message to the given client. * @param injector - the injector representing the client. @@ -173,7 +188,7 @@ class TemporaryPlayerFactory { * @throws InvocationTargetException If the message couldn't be sent. * @throws FieldAccessException If we were unable to construct the message packet. */ - private Object sendMessage(PlayerInjector injector, String message) throws InvocationTargetException, FieldAccessException { + private Object sendMessage(SocketInjector injector, String message) throws InvocationTargetException, FieldAccessException { injector.sendServerPacket(chatPacket.createPacket(message).getHandle(), false); return null; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java index f48b01d3..dc6f5e9d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java @@ -4,8 +4,6 @@ import java.io.DataInputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.util.Set; -import java.util.concurrent.TimeUnit; - import org.bukkit.entity.Player; import com.comphenix.protocol.concurrency.IntegerSet; @@ -49,12 +47,7 @@ class DummyPlayerHandler implements PlayerInjectionHandler { public void setPlayerHook(PlayerInjectHooks playerHook) { throw new UnsupportedOperationException("This is not needed in Spigot."); } - - @Override - public void scheduleDataInputRefresh(Player player) { - // Fine - } - + @Override public void addPacketHandler(int packetID) { sendingFilters.add(packetID); @@ -86,7 +79,8 @@ class DummyPlayerHandler implements PlayerInjectionHandler { } @Override - public void injectPlayer(Player player) { + public void injectPlayer(Player player, ConflictStrategy strategy) { + // We don't care about strategy injector.injectPlayer(player); } @@ -106,11 +100,6 @@ class DummyPlayerHandler implements PlayerInjectionHandler { return PlayerInjectHooks.NETWORK_SERVER_OBJECT; } - @Override - public Player getPlayerByConnection(DataInputStream inputStream, long playerTimeout, TimeUnit unit) throws InterruptedException { - throw new UnsupportedOperationException("This is not needed in Spigot."); - } - @Override public Player getPlayerByConnection(DataInputStream inputStream) throws InterruptedException { throw new UnsupportedOperationException("This is not needed in Spigot."); @@ -125,4 +114,9 @@ class DummyPlayerHandler implements PlayerInjectionHandler { public void checkListener(Set listeners) { // Yes, really } + + @Override + public void postWorldLoaded() { + // Do nothing + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java index 8dcc6014..30d68021 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -349,7 +349,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { public Object packetQueued(Object networkManager, Object connection, Object packet) { Integer id = invoker.getPacketID(packet); - if (id != null & queuedFilters.contains(id)) { + if (id != null && queuedFilters.contains(id)) { // Check for ignored packets if (ignoredPackets.remove(packet)) { return packet; @@ -427,7 +427,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { void uninjectPlayer(Player player) { final NetworkObjectInjector injector = getInjector(player); - if (player != null) { + if (player != null && injector != null) { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override public void run() { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java index 3133a434..5e908ef0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Updater.java @@ -9,6 +9,7 @@ package com.comphenix.protocol.metrics; * This class provides the means to safetly and easily update a plugin, or check to see if it is updated using dev.bukkit.org */ import java.io.*; +import java.net.ConnectException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; @@ -259,10 +260,18 @@ public class Updater logger.warning("The project slug added ('" + slug + "') is invalid, and does not exist on dev.bukkit.org"); result = Updater.UpdateResult.FAIL_BADSLUG; // Bad slug! Bad! } + if (url != null) { // Obtain the results of the project's file feed - readFeed(); + try { + readFeed(); + } catch (ConnectException ex) { + // Minimal warning - it's just a temporary problem + logger.warning("Update problem: " + ex.getMessage()); + return UpdateResult.FAIL_DBO; + } + if(versionCheck(versionTitle)) { String fileLink = getFile(versionLink); @@ -545,8 +554,9 @@ public class Updater /** * Part of RSS Reader by Vogella, modified by H31IX for use with Bukkit + * @throws ConnectException Cannot connect to the server. */ - private void readFeed() + private void readFeed() throws ConnectException { try { @@ -598,15 +608,15 @@ public class Updater /** * Open the RSS feed + * @throws ConnectException If we are unable to connect to the server. */ - private InputStream read() + private InputStream read() throws ConnectException { - try - { + try { return url.openStream(); - } - catch (IOException e) - { + } catch (ConnectException e) { + throw e; + } catch (IOException e) { throw new RuntimeException(e); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java index 9da145aa..cbf8da58 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -24,11 +24,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; /** * Retrieves fields and methods by signature, not just name. @@ -422,6 +424,22 @@ public class FuzzyReflection { throw new IllegalArgumentException("Unable to find a method that matches " + matcher); } + /** + * Retrieve every method as a map over names. + *

    + * Note that overloaded methods will only occur once in the resulting map. + * @param methods - every method. + * @return A map over every given method. + */ + public Map getMappedMethods(List methods) { + Map map = Maps.newHashMap(); + + for (Method method : methods) { + map.put(method.getName(), method); + } + return map; + } + /** * Retrieve a list of every constructor that matches the given matcher. *

    diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java index f61c2727..d8df1d37 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java @@ -21,6 +21,8 @@ import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import com.google.common.primitives.Primitives; @@ -80,7 +82,7 @@ public class PrettyPrinter { // Start and stop output.append("{ "); - printObject(output, object, start, stop, previous, hierachyDepth); + printObject(output, object, start, stop, previous, hierachyDepth, true); output.append(" }"); return output.toString(); @@ -99,15 +101,42 @@ public class PrettyPrinter { else output.append(", "); - // Handle exceptions - if (value != null) - printValue(output, value, value.getClass(), stop, previous, hierachyIndex - 1); - else - output.append("NULL"); + // Print value + printValue(output, value, stop, previous, hierachyIndex - 1); } output.append(")"); } + + /** + * Print the content of a maps entries. + * @param output - the output string builder. + * @param map - the map to print. + * @param current - the type of this map. + * @param stop - the class that indicates we should stop printing. + * @param previous - previous objects printed. + * @param hierachyIndex - hierachy index. + * @throws IllegalAccessException If any reflection went wrong. + */ + private static void printMap(StringBuilder output, Map map, Class current, Class stop, + Set previous, int hierachyIndex) throws IllegalAccessException { + + boolean first = true; + output.append("["); + + for (Entry entry : map.entrySet()) { + if (first) + first = false; + else + output.append(", "); + + printValue(output, entry.getKey(), stop, previous, hierachyIndex - 1); + output.append(": "); + printValue(output, entry.getValue(), stop, previous, hierachyIndex - 1); + } + + output.append("]"); + } private static void printArray(StringBuilder output, Object array, Class current, Class stop, Set previous, int hierachyIndex) throws IllegalAccessException { @@ -142,10 +171,8 @@ public class PrettyPrinter { // Internal recursion method private static void printObject(StringBuilder output, Object object, Class current, Class stop, - Set previous, int hierachyIndex) throws IllegalAccessException { - // Trickery - boolean first = true; - + Set previous, int hierachyIndex, boolean first) throws IllegalAccessException { + // See if we're supposed to skip this class if (current == Object.class || (stop != null && current.equals(stop))) { return; @@ -168,10 +195,11 @@ public class PrettyPrinter { Class type = field.getType(); Object value = FieldUtils.readField(field, object, true); - if (first) + if (first) { first = false; - else + } else { output.append(", "); + } output.append(field.getName()); output.append(" = "); @@ -180,10 +208,16 @@ public class PrettyPrinter { } // Recurse - printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex); + printObject(output, object, current.getSuperclass(), stop, previous, hierachyIndex, first); } - @SuppressWarnings("rawtypes") + private static void printValue(StringBuilder output, Object value, Class stop, + Set previous, int hierachyIndex) throws IllegalAccessException { + // Handle the NULL case + printValue(output, value, value != null ? value.getClass() : null, stop, previous, hierachyIndex); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) private static void printValue(StringBuilder output, Object value, Class type, Class stop, Set previous, int hierachyIndex) throws IllegalAccessException { // Just print primitive types @@ -197,12 +231,14 @@ public class PrettyPrinter { printArray(output, value, type, stop, previous, hierachyIndex); } else if (Iterable.class.isAssignableFrom(type)) { printIterables(output, (Iterable) value, type, stop, previous, hierachyIndex); + } else if (Map.class.isAssignableFrom(type)) { + printMap(output, (Map) value, type, stop, previous, hierachyIndex); } else if (ClassLoader.class.isAssignableFrom(type) || previous.contains(value)) { // Don't print previous objects output.append("\"" + value + "\""); } else { output.append("{ "); - printObject(output, value, value.getClass(), stop, previous, hierachyIndex); + printObject(output, value, value.getClass(), stop, previous, hierachyIndex, true); output.append(" }"); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java index 4359e080..836a45d1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/BackgroundCompiler.java @@ -17,6 +17,10 @@ package com.comphenix.protocol.reflect.compiler; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -29,6 +33,9 @@ import javax.annotation.Nullable; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.reflect.compiler.StructureCompiler.StructureKey; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.util.concurrent.ThreadFactoryBuilder; /** @@ -48,15 +55,26 @@ public class BackgroundCompiler { // How long to wait for a shutdown public static final int SHUTDOWN_DELAY_MS = 2000; + /** + * The default fraction of perm gen space after which the background compiler will be disabled. + */ + public static final double DEFAULT_DISABLE_AT_PERM_GEN = 0.65; + // The single background compiler we're using private static BackgroundCompiler backgroundCompiler; + // Classes we're currently compiling + private Map>> listeners = Maps.newHashMap(); + private Object listenerLock = new Object(); + private StructureCompiler compiler; private boolean enabled; private boolean shuttingDown; private ExecutorService executor; private ErrorReporter reporter; + + private double disablePermGenFraction = DEFAULT_DISABLE_AT_PERM_GEN; /** * Retrieves the current background compiler. @@ -139,26 +157,61 @@ public class BackgroundCompiler { * @param uncompiled - structure modifier to compile. * @param listener - listener responsible for responding to the compilation. */ + @SuppressWarnings({"rawtypes", "unchecked"}) public void scheduleCompilation(final StructureModifier uncompiled, final CompileListener listener) { - // Only schedule if we're enabled if (enabled && !shuttingDown) { + // Check perm gen + if (getPermGenUsage() > disablePermGenFraction) + return; // Don't try to schedule anything if (executor == null || executor.isShutdown()) return; + + // Use to look up structure modifiers + final StructureKey key = new StructureKey(uncompiled); + + // Allow others to listen in too + synchronized (listenerLock) { + List list = listeners.get(key); + + if (!listeners.containsKey(key)) { + listeners.put(key, (List) Lists.newArrayList(listener)); + } else { + // We're currently compiling + list.add(listener); + return; + } + } // Create the worker that will compile our modifier Callable worker = new Callable() { @Override public Object call() throws Exception { StructureModifier modifier = uncompiled; + List list = null; // Do our compilation try { modifier = compiler.compile(modifier); - listener.onCompiled(modifier); - + + synchronized (listenerLock) { + list = listeners.get(key); + } + + // Only execute the listeners if there is a list + if (list != null) { + for (Object compileListener : list) { + ((CompileListener) compileListener).onCompiled(modifier); + } + + // Remove it when we're done + synchronized (listenerLock) { + list = listeners.remove(key); + } + } + } catch (Throwable e) { // Disable future compilations! setEnabled(false); @@ -205,6 +258,41 @@ public class BackgroundCompiler { } } + /** + * Add a compile listener if we are still waiting for the structure modifier to be compiled. + * @param uncompiled - the structure modifier that may get compiled. + * @param listener - the listener to invoke in that case. + */ + @SuppressWarnings("unchecked") + public void addListener(final StructureModifier uncompiled, final CompileListener listener) { + synchronized (listenerLock) { + StructureKey key = new StructureKey(uncompiled); + + @SuppressWarnings("rawtypes") + List list = listeners.get(key); + + if (list != null) { + list.add(listener); + } + } + } + + /** + * Retrieve the current usage of the Perm Gen space in percentage. + * @return Usage of the perm gen space. + */ + private double getPermGenUsage() { + for (MemoryPoolMXBean item : ManagementFactory.getMemoryPoolMXBeans()) { + if (item.getName().contains("Perm Gen")) { + MemoryUsage usage = item.getUsage(); + return usage.getUsed() / (double) usage.getCommitted(); + } + } + + // Unknown + return 0; + } + /** * Clean up after ourselves using the default timeout. */ @@ -246,6 +334,22 @@ public class BackgroundCompiler { this.enabled = enabled; } + /** + * Retrieve the fraction of perm gen space used after which the background compiler will be disabled. + * @return The fraction after which the background compiler is disabled. + */ + public double getDisablePermGenFraction() { + return disablePermGenFraction; + } + + /** + * Set the fraction of perm gen space used after which the background compiler will be disabled. + * @param fraction - the maximum use of perm gen space. + */ + public void setDisablePermGenFraction(double fraction) { + this.disablePermGenFraction = fraction; + } + /** * Retrieve the current structure compiler. * @return Current structure compiler. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java index 579c069a..4a4eedd3 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/compiler/StructureCompiler.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.reflect.StructureModifier; import com.google.common.base.Objects; import com.google.common.primitives.Primitives; @@ -94,7 +95,7 @@ public final class StructureCompiler { // Used to store generated classes of different types @SuppressWarnings("rawtypes") - private static class StructureKey { + static class StructureKey { private Class targetType; private Class fieldType; @@ -206,6 +207,11 @@ public final class StructureCompiler { return (StructureModifier) compiledClass.getConstructor( StructureModifier.class, StructureCompiler.class). newInstance(source, this); + } catch (OutOfMemoryError e) { + // Print the number of generated classes by the current instances + ProtocolLibrary.getErrorReporter().reportWarning( + this, "May have generated too many classes (count: " + compiledCache.size() + ")"); + throw e; } catch (IllegalArgumentException e) { throw new IllegalStateException("Used invalid parameters in instance creation", e); } catch (SecurityException e) { @@ -218,7 +224,7 @@ public final class StructureCompiler { throw new RuntimeException("Error occured while instancing generated class.", e); } catch (NoSuchMethodException e) { throw new IllegalStateException("Cannot happen.", e); - } + } } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java index 9081705e..a629ba1b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java @@ -58,7 +58,7 @@ class CachedPackage { if (result == null) { // Look up the class dynamically result = CachedPackage.class.getClassLoader(). - loadClass(packageName + "." + className); + loadClass(combine(packageName, className)); cache.put(className, result); } @@ -68,4 +68,16 @@ class CachedPackage { throw new RuntimeException("Cannot find class " + className, e); } } + + /** + * Correctly combine a package name and the child class we're looking for. + * @param packageName - name of the package, or an empty string for the default package. + * @param className - the class name. + * @return We full class path. + */ + private String combine(String packageName, String className) { + if (packageName.length() == 0) + return className; + return packageName + "." + className; + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java index 92af6cca..7fc274d0 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ChatExtensions.java @@ -27,6 +27,7 @@ import com.comphenix.protocol.Packets; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.injector.PacketConstructor; import com.comphenix.protocol.reflect.FieldAccessException; +import com.google.common.base.Strings; /** * Utility methods for sending chat messages. @@ -98,4 +99,48 @@ public class ChatExtensions { } } } + + /** + * Print a flower box around a given message. + * @param message - the message to print. + * @param marginChar - the character to use as margin. + * @param marginWidth - the width (in characters) of the left and right margin. + * @param marginHeight - the height (in characters) of the top and buttom margin. + */ + public static String[] toFlowerBox(String[] message, String marginChar, int marginWidth, int marginHeight) { + String[] output = new String[message.length + marginHeight * 2]; + int width = getMaximumLength(message); + + // Margins + String topButtomMargin = Strings.repeat(marginChar, width + marginWidth * 2); + String leftRightMargin = Strings.repeat(marginChar, marginWidth); + + // Add left and right margin + for (int i = 0; i < message.length; i++) { + output[i + marginHeight] = leftRightMargin + Strings.padEnd(message[i], width, ' ') + leftRightMargin; + } + + // Insert top and bottom margin + for (int i = 0; i < marginHeight; i++) { + output[i] = topButtomMargin; + output[output.length - i - 1] = topButtomMargin; + } + return output; + } + + /** + * Retrieve the longest line lenght in a list of strings. + * @param lines - the lines. + * @return Longest line lenght. + */ + private static int getMaximumLength(String[] lines) { + int current = 0; + + // Find the longest line + for (int i = 0; i < lines.length; i++) { + if (current < lines[i].length()) + current = lines[i].length(); + } + return current; + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java new file mode 100644 index 00000000..f5706fa4 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftMethods.java @@ -0,0 +1,64 @@ +package com.comphenix.protocol.utility; + +import java.lang.reflect.Method; +import java.util.Map; + +import com.comphenix.protocol.reflect.FuzzyReflection; + +/** + * Static methods for accessing Minecraft methods. + * + * @author Kristian + */ +public class MinecraftMethods { + // For player connection + private volatile static Method sendPacketMethod; + + /** + * Retrieve the send packet method in PlayerConnection/NetServerHandler. + * @return The send packet method. + */ + public static Method getSendPacketMethod() { + if (sendPacketMethod == null) { + Class serverHandlerClass = MinecraftReflection.getNetServerHandlerClass(); + + try { + sendPacketMethod = FuzzyReflection.fromObject(serverHandlerClass).getMethodByName("sendPacket.*"); + } catch (IllegalArgumentException e) { + Map netServer = getMethodList( + serverHandlerClass, MinecraftReflection.getPacketClass()); + Map netHandler = getMethodList( + MinecraftReflection.getNetHandlerClass(), MinecraftReflection.getPacketClass()); + + // Remove every method in net handler from net server + for (String methodName : netHandler.keySet()) { + netServer.remove(methodName); + } + + // The remainder is the send packet method + if (netServer.size() == 1) { + Method[] methods = netServer.values().toArray(new Method[0]); + sendPacketMethod = methods[0]; + } else { + throw new IllegalArgumentException("Unable to find the sendPacket method in NetServerHandler/PlayerConnection."); + } + } + } + return sendPacketMethod; + } + + /** + * Retrieve a method mapped list of every method with the given signature. + * @param source - class source. + * @param params - parameters. + * @return Method mapped list. + */ + private static Map getMethodList(Class source, Class... params) { + FuzzyReflection reflect = FuzzyReflection.fromClass(source, true); + + return reflect.getMappedMethods( + reflect.getMethodListByParameters(Void.TYPE, params) + ); + } + +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 3717fe1d..399082d1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -73,7 +73,7 @@ public class MinecraftReflection { private static String MINECRAFT_FULL_PACKAGE = null; private static String CRAFTBUKKIT_PACKAGE = null; - + private static CachedPackage minecraftPackage; private static CachedPackage craftbukkitPackage; @@ -85,7 +85,7 @@ public class MinecraftReflection { private static Method craftNMSMethod; private static Method craftBukkitMethod; private static boolean craftItemStackFailed; - + // net.minecraft.server private static Class itemStackArrayClass; @@ -125,15 +125,15 @@ public class MinecraftReflection { // This server should have a "getHandle" method that we can use if (craftServer != null) { try { - Class craftClass = craftServer.getClass(); - Method getHandle = craftClass.getMethod("getHandle"); - - Class returnType = getHandle.getReturnType(); - String returnName = returnType.getCanonicalName(); - // The return type will tell us the full package, regardless of formating + Class craftClass = craftServer.getClass(); CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName()); - MINECRAFT_FULL_PACKAGE = getPackage(returnName); + + // Next, do the same for CraftEntity.getHandle() in order to get the correct Minecraft package + Class craftEntity = getCraftEntityClass(); + Method getHandle = craftEntity.getMethod("getHandle"); + + MINECRAFT_FULL_PACKAGE = getPackage(getHandle.getReturnType().getCanonicalName()); // Pretty important invariant if (!MINECRAFT_FULL_PACKAGE.startsWith(MINECRAFT_PREFIX_PACKAGE)) { @@ -165,7 +165,7 @@ public class MinecraftReflection { throw new IllegalStateException("Could not find Bukkit. Is it running?"); } } - + /** * Used during debugging and testing. * @param minecraftPackage - the current Minecraft package. @@ -185,7 +185,8 @@ public class MinecraftReflection { */ public static String getCraftBukkitPackage() { // Ensure it has been initialized - getMinecraftPackage(); + if (CRAFTBUKKIT_PACKAGE == null) + getMinecraftPackage(); return CRAFTBUKKIT_PACKAGE; } @@ -490,22 +491,29 @@ public class MinecraftReflection { public static Class getMinecraftServerClass() { try { return getMinecraftClass("MinecraftServer"); - } catch (RuntimeException e) { - // Get the first constructor that matches CraftServer(MINECRAFT_OBJECT, ANY) - Constructor selected = FuzzyReflection.fromClass(getCraftBukkitClass("CraftServer")). - getConstructor(FuzzyMethodContract.newBuilder(). - parameterMatches(getMinecraftObjectMatcher(), 0). - parameterCount(2). - build() - ); - Class[] params = selected.getParameterTypes(); - - // Jackpot - two classes at the same time! - setMinecraftClass("MinecraftServer", params[0]); - setMinecraftClass("ServerConfigurationManager", params[1]); - return params[0]; + } catch (RuntimeException e) { + useFallbackServer(); + return getMinecraftClass("MinecraftServer"); } } + + /** + * Fallback method that can determine the MinecraftServer and the ServerConfigurationManager. + */ + private static void useFallbackServer() { + // Get the first constructor that matches CraftServer(MINECRAFT_OBJECT, ANY) + Constructor selected = FuzzyReflection.fromClass(getCraftBukkitClass("CraftServer")). + getConstructor(FuzzyMethodContract.newBuilder(). + parameterMatches(getMinecraftObjectMatcher(), 0). + parameterCount(2). + build() + ); + Class[] params = selected.getParameterTypes(); + + // Jackpot - two classes at the same time! + setMinecraftClass("MinecraftServer", params[0]); + setMinecraftClass("ServerConfigurationManager", params[1]); + } /** * Retrieve the player list class (or ServerConfigurationManager), @@ -516,7 +524,7 @@ public class MinecraftReflection { return getMinecraftClass("ServerConfigurationManager", "PlayerList"); } catch (RuntimeException e) { // Try again - getMinecraftServerClass(); + useFallbackServer(); return getMinecraftClass("ServerConfigurationManager"); } } @@ -553,7 +561,7 @@ public class MinecraftReflection { return getMinecraftClass("NetServerHandler", "PlayerConnection"); } catch (RuntimeException e) { // Use the player connection field - return setMinecraftClass("NetLoginHandler", + return setMinecraftClass("NetServerHandler", FuzzyReflection.fromClass(getEntityPlayerClass()). getFieldByType("playerConnection", getNetHandlerClass()).getType() ); @@ -908,7 +916,15 @@ public class MinecraftReflection { public static Class getCraftPlayerClass() { return getCraftBukkitClass("entity.CraftPlayer"); } - + + /** + * Retrieve the CraftEntity class. + * @return CraftEntity class. + */ + public static Class getCraftEntityClass() { + return getCraftBukkitClass("entity.CraftEntity"); + } + /** * Retrieve a CraftItemStack from a given ItemStack. * @param bukkitItemStack - the Bukkit ItemStack to convert. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/MemoryElement.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/MemoryElement.java new file mode 100644 index 00000000..e8287954 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/MemoryElement.java @@ -0,0 +1,65 @@ +package com.comphenix.protocol.wrappers.nbt; + +class MemoryElement implements NbtBase { + private String name; + private TType value; + private NbtType type; + + public MemoryElement(String name, TType value) { + if (name == null) + throw new IllegalArgumentException("Name cannot be NULL."); + if (value == null) + throw new IllegalArgumentException("Element cannot be NULL."); + + this.name = name; + this.value = value; + this.type = NbtType.getTypeFromClass(value.getClass()); + } + + public MemoryElement(String name, TType value, NbtType type) { + if (name == null) + throw new IllegalArgumentException("Name cannot be NULL."); + if (type == null) + throw new IllegalArgumentException("Type cannot be NULL."); + + this.name = name; + this.value = value; + this.type = type; + } + + @Override + public boolean accept(NbtVisitor visitor) { + return visitor.visit(this); + } + + @Override + public NbtType getType() { + return type; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public TType getValue() { + return value; + } + + @Override + public void setValue(TType newValue) { + this.value = newValue; + } + + @Override + public NbtBase deepClone() { + // This assumes value is an immutable object + return new MemoryElement(name, value, type); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java index 4fda61b8..87f9ffe4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtBase.java @@ -64,6 +64,9 @@ public interface NbtBase { * Is either a primitive {@link java.lang.Number wrapper}, {@link java.lang.String String}, * {@link java.util.List List} or a {@link java.util.Map Map}. *

    + * Users are encouraged to cast an NBT compound to {@link NbtCompound} and use its put and get-methods + * instead of accessing its content from getValue(). + *

    * All operations that modify collections directly, such as {@link java.util.List#add(Object) List.add(Object)} or * {@link java.util.Map#clear() Map.clear()}, are considered optional. This also include members in {@link java.util.Iterator Iterator} and * {@link java.util.ListIterator ListIterator}. Operations that are not implemented throw a diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java index 5465ae29..226d7b61 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/NbtCompound.java @@ -5,6 +5,8 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; + /** * Represents a mapping of arbitrary NBT elements and their unique names. *

    @@ -16,6 +18,10 @@ import java.util.Set; * @author Kristian */ public interface NbtCompound extends NbtBase>>, Iterable> { + @Override + @Deprecated() + public Map> getValue(); + /** * Determine if an entry with the given key exists or not. * @param key - the key to lookup. @@ -48,8 +54,9 @@ public interface NbtCompound extends NbtBase>>, Iterable< * Set a entry based on its name. * @param entry - entry with a name and value. * @return This compound, for chaining. + * @throws IllegalArgumentException If entry is NULL. */ - public abstract NbtCompound put(NbtBase entry); + public abstract NbtCompound put(@Nonnull NbtBase entry); /** * Retrieve the string value of an entry identified by a given key. @@ -251,7 +258,26 @@ public interface NbtCompound extends NbtBase>>, Iterable< * @return This current compound, for chaining. */ public abstract NbtCompound put(String key, int[] value); - + + /** + * Associates a given Java primitive value, list, map or NbtBase with a certain key. + *

    + * If the value is NULL, the corresponding key is removed. Any Map or List will be converted + * to a corresponding NbtCompound or NbtList. + * + * @param key - the name of the new entry, + * @param value - the value of the new entry, or NULL to remove the current value. + * @return This current compound, for chaining. + */ + public abstract NbtCompound putObject(String key, Object value); + + /** + * Retrieve the primitive object, NbtList or NbtCompound associated with the given key. + * @param key - the key of the object to find. + * @return The object with this key, or NULL if we couldn't find anything. + */ + public abstract Object getObject(String key); + /** * Retrieve the compound (map) value of an entry identified by a given key. * @param key - the key of the entry. @@ -304,6 +330,13 @@ public interface NbtCompound extends NbtBase>>, Iterable< */ public abstract NbtCompound put(String key, Collection> list); + /** + * Remove the NBT element that is associated with the given key. + * @param key - the key of the element to remove. + * @return The removed element, or NULL if no such element was found. + */ + public abstract NbtBase remove(String key); + /** * Retrieve an iterator view of the NBT tags stored in this compound. * @return The tags stored in this compound. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java index 8d3f6fa8..ae277973 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/nbt/WrappedCompound.java @@ -152,7 +152,13 @@ class WrappedCompound implements NbtWrapper>>, Iterable> newValue) { // Write all the entries for (Map.Entry> entry : newValue.entrySet()) { - put(entry.getValue()); + Object value = entry.getValue(); + + // We don't really know + if (value instanceof NbtBase) + put(entry.getValue()); + else + putObject(entry.getKey(), entry.getValue()); } } @@ -215,6 +221,9 @@ class WrappedCompound implements NbtWrapper>>, Iterable NbtCompound put(NbtBase entry) { + if (entry == null) + throw new IllegalArgumentException("Entry cannot be NULL."); + getValue().put(entry.getName(), entry); return this; } @@ -252,6 +261,29 @@ class WrappedCompound implements NbtWrapper>>, Iterable) value); + } else { + NbtBase base = new MemoryElement(key, value); + put(base); + } + return this; + } + + @Override + public Object getObject(String key) { + NbtBase base = getValue(key); + + if (base != null && base.getType() != NbtType.TAG_LIST && base.getType() != NbtType.TAG_COMPOUND) + return base.getValue(); + else + return base; + } + /** * Retrieve the byte value of an entry identified by a given key. * @param key - the key of the entry. @@ -565,6 +597,9 @@ class WrappedCompound implements NbtWrapper>>, Iterable entry) { + if (entry == null) + throw new IllegalArgumentException("Entry cannot be NULL."); + // Don't modify the original NBT NbtBase clone = entry.deepClone(); @@ -583,6 +618,11 @@ class WrappedCompound implements NbtWrapper>>, Iterable NbtBase remove(String key) { + return getValue().remove(key); + } + @Override public void write(DataOutput destination) { NbtBinarySerializer.DEFAULT.serialize(container, destination); diff --git a/ProtocolLib/src/main/resources/config.yml b/ProtocolLib/src/main/resources/config.yml index e3c48aff..46869ad7 100644 --- a/ProtocolLib/src/main/resources/config.yml +++ b/ProtocolLib/src/main/resources/config.yml @@ -18,5 +18,4 @@ global: ignore version check: # Override the starting injecting method - injection method: - \ No newline at end of file + injection method: \ No newline at end of file diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 50beb220..23fb34bf 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,9 +1,8 @@ name: ProtocolLib -version: 2.2.0 +version: 2.3.0 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib -load: startup main: com.comphenix.protocol.ProtocolLibrary database: false