diff --git a/ProtocolLib/pom.xml b/ProtocolLib/pom.xml index 9c0c920b..e1268256 100644 --- a/ProtocolLib/pom.xml +++ b/ProtocolLib/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.comphenix.protocol ProtocolLib - 2.1.0 + 2.2.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 f4940bee..57f31fe7 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CleanupStaticMembers.java @@ -78,9 +78,9 @@ class CleanupStaticMembers { "com.comphenix.protocol.injector.player.PlayerInjector", "com.comphenix.protocol.injector.player.TemporaryPlayerFactory", "com.comphenix.protocol.injector.EntityUtilities", - "com.comphenix.protocol.injector.MinecraftRegistry", - "com.comphenix.protocol.injector.PacketInjector", - "com.comphenix.protocol.injector.ReadPacketModifier", + "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" diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java index 4da7a3c4..6f098599 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/CommandPacket.java @@ -175,7 +175,7 @@ class CommandPacket extends CommandBase { try { chatter.broadcastMessageSilently(message, permission); } catch (InvocationTargetException e) { - reporter.reportDetailed(this, "Cannot send chat message.", e, message, message); + reporter.reportDetailed(this, "Cannot send chat message.", e, message, permission); } } @@ -415,7 +415,7 @@ class CommandPacket extends CommandBase { Class clazz = packet.getClass(); // Get the first Minecraft super class - while ((!clazz.getName().startsWith("net.minecraft.server") || + while ((!MinecraftReflection.isMinecraftClass(clazz) || Factory.class.isAssignableFrom(clazz)) && clazz != Object.class) { clazz = clazz.getSuperclass(); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java index 1509c411..ada35964 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/Packets.java @@ -30,6 +30,11 @@ import com.comphenix.protocol.reflect.IntEnum; */ public final class Packets { + /** + * The highest possible packet ID. It's unlikely that this value will ever change. + */ + public static final int MAXIMUM_PACKET_ID = 255; + /** * List of packets sent only by the server. * @author Kristian @@ -60,7 +65,7 @@ public final class Packets { public static final int ARM_ANIMATION = 18; public static final int NAMED_ENTITY_SPAWN = 20; /** - * Removed in 1.4.6 and replaced with {@link VEHICLE_SPAWN}. + * Removed in 1.4.6 and replaced with VEHICLE_SPAWN. * @see Protocol History - MinecraftCoalition */ @Deprecated() diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java index 9a5bd6ac..28c1ef9d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/ProtocolLibrary.java @@ -282,7 +282,7 @@ public class ProtocolLibrary extends JavaPlugin { Pattern ourPlugin = Pattern.compile("ProtocolLib-(.*)\\.jar"); MinecraftVersion currentVersion = new MinecraftVersion(this.getDescription().getVersion()); MinecraftVersion newestVersion = null; - + // Skip the file that contains this current instance however File loadedFile = getFile(); @@ -297,7 +297,10 @@ public class ProtocolLibrary extends JavaPlugin { if (match.matches()) { MinecraftVersion version = new MinecraftVersion(match.group(1)); - if (newestVersion == null || newestVersion.compareTo(version) < 0) { + if (candidate.length() == 0) { + // Delete and inform the user + logger.info((candidate.delete() ? "Deleted " : "Could not delete ") + candidate); + } else if (newestVersion == null || newestVersion.compareTo(version) < 0) { newestVersion = version; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java index 364b6c1f..800124f2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/async/PacketProcessingQueue.java @@ -19,6 +19,7 @@ package com.comphenix.protocol.async; import java.util.Collection; import java.util.Iterator; +import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.Semaphore; @@ -67,10 +68,19 @@ class PacketProcessingQueue extends AbstractConcurrentListenerMultimapcreate(), null); + try { + this.processingQueue = Synchronization.queue(MinMaxPriorityQueue. + expectedSize(initialSize). + maximumSize(maximumSize). + create(), null); + } catch (IncompatibleClassChangeError e) { + System.out.println("[ProtocolLib] Guava is either missing or corrupt. Reverting to PriorityQueue."); + e.printStackTrace(); + + // It's a Beta class after all + this.processingQueue = Synchronization.queue( + new PriorityQueue(), null); + } this.maximumConcurrency = maximumConcurrency; this.concurrentProcessing = new Semaphore(maximumConcurrency); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/IntegerSet.java b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java similarity index 94% rename from ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/IntegerSet.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java index 3deea142..e2aaa3f6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/IntegerSet.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/concurrency/IntegerSet.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol.injector.player; +package com.comphenix.protocol.concurrency; import java.util.Arrays; import java.util.HashSet; @@ -27,7 +27,7 @@ import java.util.Set; * This class is intentionally missing a size method. * @author Kristian */ -class IntegerSet { +public class IntegerSet { private final boolean[] array; /** @@ -44,7 +44,7 @@ class IntegerSet { /** * Determine whether or not the given element exists in the set. - * @param value - the element to check. Must be in the range [0, count). + * @param element - the element to check. Must be in the range [0, count). * @return TRUE if the given element exists, FALSE otherwise. */ public boolean contains(int element) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java index 2362ed55..31152925 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/EntityUtilities.java @@ -164,9 +164,9 @@ class EntityUtilities { BukkitUnwrapper unwrapper = new BukkitUnwrapper(); Object worldServer = unwrapper.unwrapItem(world); - // We have to rely on the class naming here. if (entityTrackerField == null) - entityTrackerField = FuzzyReflection.fromObject(worldServer).getFieldByType(".*Tracker"); + entityTrackerField = FuzzyReflection.fromObject(worldServer). + getFieldByType("tracker", MinecraftReflection.getEntityTrackerClass()); // Get the tracker Object tracker = null; @@ -191,7 +191,7 @@ class EntityUtilities { // The Minecraft field that's NOT filled in by the constructor trackedEntitiesField = FuzzyReflection.fromObject(tracker, true). - getFieldByType(MinecraftReflection.MINECRAFT_OBJECT, ignoredTypes); + getFieldByType(MinecraftReflection.getMinecraftObjectRegex(), ignoredTypes); } // Read the entity hashmap @@ -250,8 +250,16 @@ class EntityUtilities { // Handle NULL cases if (trackerEntry != null) { - if (trackerField == null) - trackerField = trackerEntry.getClass().getField("tracker"); + if (trackerField == null) { + try { + trackerField = trackerEntry.getClass().getField("tracker"); + } catch (NoSuchFieldException e) { + // Assume it's the first public entity field then + trackerField = FuzzyReflection.fromObject(trackerEntry).getFieldByType( + "tracker", MinecraftReflection.getEntityClass()); + } + } + tracker = FieldUtils.readField(trackerField, trackerEntry, true); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java index 3d138a13..ce5f5b1f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketConstructor.java @@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -114,7 +115,7 @@ public class PacketConstructor { } } - Class packetType = MinecraftRegistry.getPacketClassFromID(id, true); + Class packetType = PacketRegistry.getPacketClassFromID(id, true); if (packetType == null) throw new IllegalArgumentException("Could not find a packet by the id " + id); 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 e43921ef..eea43aae 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketFilterManager.java @@ -51,7 +51,12 @@ import com.comphenix.protocol.async.AsyncFilterManager; import com.comphenix.protocol.async.AsyncMarker; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.*; +import com.comphenix.protocol.injector.packet.PacketInjector; +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.spigot.SpigotPacketInjector; import com.comphenix.protocol.reflect.FieldAccessException; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.utility.MinecraftReflection; @@ -138,6 +143,9 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok // Whether or not plugins are using the send/receive methods private AtomicBoolean packetCreation = new AtomicBoolean(); + // Spigot listener, if in use + private SpigotPacketInjector spigotInjector; + /** * Only create instances of this class if protocol lib is disabled. * @param unhookTask @@ -177,15 +185,37 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok }; try { - // Initialize injection mangers - this.playerInjection = new PlayerInjectionHandler(classLoader, reporter, isInjectionNecessary, this, packetListeners, server); - this.packetInjector = new PacketInjector(classLoader, this, playerInjection, reporter); + // Spigot + if (SpigotPacketInjector.canUseSpigotListener()) { + spigotInjector = new SpigotPacketInjector(classLoader, reporter, this, server); + this.playerInjection = spigotInjector.getPlayerHandler(); + this.packetInjector = spigotInjector.getPacketInjector(); + + } else { + // Initialize standard injection mangers + this.playerInjection = PlayerInjectorBuilder.newBuilder(). + invoker(this). + server(server). + reporter(reporter). + classLoader(classLoader). + packetListeners(packetListeners). + injectionFilter(isInjectionNecessary). + buildHandler(); + + this.packetInjector = PacketInjectorBuilder.newBuilder(). + invoker(this). + reporter(reporter). + classLoader(classLoader). + playerInjection(playerInjection). + buildInjector(); + } + this.asyncFilterManager = new AsyncFilterManager(reporter, server.getScheduler(), this); // Attempt to load the list of server and client packets try { - this.serverPackets = MinecraftRegistry.getServerPackets(); - this.clientPackets = MinecraftRegistry.getClientPackets(); + this.serverPackets = PacketRegistry.getServerPackets(); + this.clientPackets = PacketRegistry.getClientPackets(); } catch (FieldAccessException e) { reporter.reportWarning(this, "Cannot load server and client packet list.", e); } @@ -600,6 +630,8 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok * @param plugin - the parent plugin. */ public void registerEvents(PluginManager manager, final Plugin plugin) { + if (spigotInjector != null && !spigotInjector.register(plugin)) + throw new IllegalArgumentException("Spigot has already been registered."); try { manager.registerEvents(new Listener() { @@ -692,22 +724,22 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok if (!MinecraftReflection.isPacketClass(packet)) throw new IllegalArgumentException("The given object " + packet + " is not a packet."); - return MinecraftRegistry.getPacketToID().get(packet.getClass()); + return PacketRegistry.getPacketToID().get(packet.getClass()); } @Override public void registerPacketClass(Class clazz, int packetID) { - MinecraftRegistry.getPacketToID().put(clazz, packetID); + PacketRegistry.getPacketToID().put(clazz, packetID); } @Override public void unregisterPacketClass(Class clazz) { - MinecraftRegistry.getPacketToID().remove(clazz); + PacketRegistry.getPacketToID().remove(clazz); } @Override public Class getPacketClassFromID(int packetID, boolean forceVanilla) { - return MinecraftRegistry.getPacketClassFromID(packetID, forceVanilla); + return PacketRegistry.getPacketClassFromID(packetID, forceVanilla); } // Yes, this is crazy. @@ -823,7 +855,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok * @throws FieldAccessException If we're unable to retrieve the server packet data from Minecraft. */ public static Set getServerPackets() throws FieldAccessException { - return MinecraftRegistry.getServerPackets(); + return PacketRegistry.getServerPackets(); } /** @@ -832,7 +864,7 @@ public final class PacketFilterManager implements ProtocolManager, ListenerInvok * @throws FieldAccessException If we're unable to retrieve the client packet data from Minecraft. */ public static Set getClientPackets() throws FieldAccessException { - return MinecraftRegistry.getClientPackets(); + return PacketRegistry.getClientPackets(); } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java index b76f2179..d5455e9f 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/StructureCache.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; import com.comphenix.protocol.reflect.compiler.CompileListener; @@ -46,7 +47,7 @@ public class StructureCache { */ public static Object newPacket(int id) { try { - return MinecraftRegistry.getPacketClassFromID(id, true).newInstance(); + return PacketRegistry.getPacketClassFromID(id, true).newInstance(); } catch (InstantiationException e) { return null; } catch (IllegalAccessException e) { @@ -82,7 +83,7 @@ public class StructureCache { */ public static StructureModifier getStructure(Class packetType, boolean compile) { // Get the ID from the class - return getStructure(MinecraftRegistry.getPacketID(packetType), compile); + return getStructure(PacketRegistry.getPacketID(packetType), compile); } /** @@ -99,7 +100,7 @@ public class StructureCache { if (result == null) { // Use the vanilla class definition final StructureModifier value = new StructureModifier( - MinecraftRegistry.getPacketClassFromID(id, true), MinecraftReflection.getPacketClass(), true); + PacketRegistry.getPacketClassFromID(id, true), MinecraftReflection.getPacketClass(), true); result = structureModifiers.putIfAbsent(id, value); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java new file mode 100644 index 00000000..4276212c --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjector.java @@ -0,0 +1,62 @@ +package com.comphenix.protocol.injector.packet; + +import java.util.Set; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; + +/** + * Represents a incoming packet injector. + * + * @author Kristian + */ +public interface PacketInjector { + /** + * Undo a packet cancel. + * @param id - the id of the packet. + * @param packet - packet to uncancel. + */ + public abstract void undoCancel(Integer id, Object packet); + + /** + * Start intercepting packets with the given packet ID. + * @param packetID - the ID of the packets to start intercepting. + * @return TRUE if we didn't already intercept these packets, FALSE otherwise. + */ + public abstract boolean addPacketHandler(int packetID); + + /** + * Stop intercepting packets with the given packet ID. + * @param packetID - the ID of the packets to stop intercepting. + * @return TRUE if we successfuly stopped intercepting a given packet ID, FALSE otherwise. + */ + public abstract boolean removePacketHandler(int packetID); + + /** + * Determine if packets with the given packet ID is being intercepted. + * @param packetID - the packet ID to lookup. + * @return TRUE if we do, FALSE otherwise. + */ + public abstract boolean hasPacketHandler(int packetID); + + /** + * Retrieve every intercepted packet ID. + * @return Every intercepted packet ID. + */ + public abstract Set getPacketHandlers(); + + /** + * Let the packet listeners process the given packet. + * @param packet - a packet to process. + * @param client - the client that sent the packet. + * @return The resulting packet event. + */ + public abstract PacketEvent packetRecieved(PacketContainer packet, Player client); + + /** + * Perform any necessary cleanup before unloading ProtocolLib. + */ + public abstract void cleanupAll(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java new file mode 100644 index 00000000..c3720b77 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketInjectorBuilder.java @@ -0,0 +1,109 @@ +package com.comphenix.protocol.injector.packet; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PacketFilterManager; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.google.common.base.Preconditions; + +/** + * A builder responsible for creating incoming packet injectors. + * + * @author Kristian + */ +public class PacketInjectorBuilder { + protected PacketInjectorBuilder() { + // No need to construct this + } + + /** + * Retrieve a new packet injector builder. + * @return Injector builder. + */ + public static PacketInjectorBuilder newBuilder() { + return new PacketInjectorBuilder(); + } + + protected ClassLoader classLoader; + protected ListenerInvoker invoker; + protected ErrorReporter reporter; + protected PlayerInjectionHandler playerInjection; + + /** + * Set the class loader to use during class generation. + * @param classLoader - new class loader. + * @return This builder, for chaining. + */ + public PacketInjectorBuilder classLoader(@Nonnull ClassLoader classLoader) { + Preconditions.checkNotNull(classLoader, "classLoader cannot be NULL"); + this.classLoader = classLoader; + return this; + } + + /** + * The error reporter used by the created injector. + * @param reporter - new error reporter. + * @return This builder, for chaining. + */ + public PacketInjectorBuilder reporter(@Nonnull ErrorReporter reporter) { + Preconditions.checkNotNull(reporter, "reporter cannot be NULL"); + this.reporter = reporter; + return this; + } + + /** + * The packet stream invoker. + * @param invoker - the invoker. + * @return This builder, for chaining. + */ + public PacketInjectorBuilder invoker(@Nonnull ListenerInvoker invoker) { + Preconditions.checkNotNull(invoker, "invoker cannot be NULL"); + this.invoker = invoker; + return this; + } + + /** + * The packet stream invoker. + * @param invoker - the invoker. + * @return This builder, for chaining. + */ + @Nonnull + public PacketInjectorBuilder playerInjection(@Nonnull PlayerInjectionHandler playerInjection) { + Preconditions.checkNotNull(playerInjection, "playerInjection cannot be NULL"); + this.playerInjection = playerInjection; + return this; + } + + /** + * Called before an object is created with this builder. + */ + private void initializeDefaults() { + ProtocolManager manager = ProtocolLibrary.getProtocolManager(); + + // Initialize with default values if we can + if (classLoader == null) + classLoader = this.getClass().getClassLoader(); + if (reporter == null) + reporter = ProtocolLibrary.getErrorReporter(); + if (invoker == null) + invoker = (PacketFilterManager) manager; + if (playerInjection == null) + throw new IllegalStateException("Player injection parameter must be initialized."); + } + + /** + * Create a packet injector using the provided fields or the default values. + *

+ * Note that any non-null builder parameters must be set. + * @return The created injector. + * @throws IllegalAccessException If anything goes wrong in terms of reflection. + */ + public PacketInjector buildInjector() throws IllegalAccessException { + initializeDefaults(); + return new ProxyPacketInjector(classLoader, invoker, playerInjection, reporter); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java similarity index 97% rename from ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java index 7ad014cb..73f4e587 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/MinecraftRegistry.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/PacketRegistry.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol.injector; +package com.comphenix.protocol.injector.packet; import java.lang.reflect.Field; import java.util.HashMap; @@ -33,12 +33,12 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableSet; /** - * Static registries in Minecraft. + * Static packet registry in Minecraft. * * @author Kristian */ @SuppressWarnings("rawtypes") -class MinecraftRegistry { +public class PacketRegistry { // Fuzzy reflection private static FuzzyReflection packetRegistry; @@ -174,7 +174,7 @@ class MinecraftRegistry { for (Map.Entry entry : getPacketToID().entrySet()) { if (Objects.equal(entry.getValue(), packetID)) { // Attempt to get the vanilla class here too - if (!forceVanilla || entry.getKey().getName().startsWith("net.minecraft.server")) + if (!forceVanilla || MinecraftReflection.isMinecraftClass(entry.getKey())) return removeEnhancer(entry.getKey(), forceVanilla); } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java similarity index 84% rename from ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java index 0a83c8ec..bd3274a8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/PacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol.injector; +package com.comphenix.protocol.injector.packet; import java.io.DataInputStream; import java.lang.reflect.Field; @@ -33,6 +33,7 @@ import net.sf.cglib.proxy.Enhancer; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; +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; @@ -43,7 +44,7 @@ import com.comphenix.protocol.utility.MinecraftReflection; * * @author Kristian */ -class PacketInjector { +class ProxyPacketInjector implements PacketInjector { // The "put" method that associates a packet ID with a packet class private static Method putMethod; @@ -64,7 +65,7 @@ class PacketInjector { // Class loader private ClassLoader classLoader; - public PacketInjector(ClassLoader classLoader, ListenerInvoker manager, + public ProxyPacketInjector(ClassLoader classLoader, ListenerInvoker manager, PlayerInjectionHandler playerInjection, ErrorReporter reporter) throws IllegalAccessException { this.classLoader = classLoader; @@ -80,6 +81,7 @@ class PacketInjector { * @param id - the id of the packet. * @param packet - packet to uncancel. */ + @Override public void undoCancel(Integer id, Object packet) { ReadPacketModifier modifier = readModifier.get(id); @@ -93,7 +95,7 @@ class PacketInjector { if (intHashMap == null) { // We're looking for the first static field with a Minecraft-object. This should be a IntHashMap. Field intHashMapField = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass(), true). - getFieldByType(MinecraftReflection.MINECRAFT_OBJECT); + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); try { intHashMap = FieldUtils.readField(intHashMapField, (Object) null, true); @@ -106,6 +108,7 @@ class PacketInjector { } } + @Override @SuppressWarnings("rawtypes") public boolean addPacketHandler(int packetID) { if (hasPacketHandler(packetID)) @@ -118,17 +121,17 @@ class PacketInjector { // * Object removeObject(int par1) // So, we'll use the classMapToInt registry instead. - Map overwritten = MinecraftRegistry.getOverwrittenPackets(); - Map previous = MinecraftRegistry.getPreviousPackets(); - Map registry = MinecraftRegistry.getPacketToID(); - Class old = MinecraftRegistry.getPacketClassFromID(packetID); + Map overwritten = PacketRegistry.getOverwrittenPackets(); + Map previous = PacketRegistry.getPreviousPackets(); + Map registry = PacketRegistry.getPacketToID(); + Class old = PacketRegistry.getPacketClassFromID(packetID); // If this packet is not known if (old == null) { throw new IllegalStateException("Packet ID " + packetID + " is not a valid packet ID in this version."); } // Check for previous injections - if (!old.getName().startsWith("net.minecraft.")) { + if (!MinecraftReflection.isMinecraftClass(old)) { throw new IllegalStateException("Packet " + packetID + " has already been injected."); } @@ -162,19 +165,20 @@ class PacketInjector { } } + @Override @SuppressWarnings("rawtypes") public boolean removePacketHandler(int packetID) { if (!hasPacketHandler(packetID)) return false; - Map registry = MinecraftRegistry.getPacketToID(); - Map previous = MinecraftRegistry.getPreviousPackets(); - Map overwritten = MinecraftRegistry.getOverwrittenPackets(); + Map registry = PacketRegistry.getPacketToID(); + Map previous = PacketRegistry.getPreviousPackets(); + Map overwritten = PacketRegistry.getOverwrittenPackets(); // Use the old class definition try { Class old = previous.get(packetID); - Class proxy = MinecraftRegistry.getPacketClassFromID(packetID); + Class proxy = PacketRegistry.getPacketClassFromID(packetID); putMethod.invoke(intHashMap, packetID, old); previous.remove(packetID); @@ -193,16 +197,18 @@ class PacketInjector { } } + @Override public boolean hasPacketHandler(int packetID) { - return MinecraftRegistry.getPreviousPackets().containsKey(packetID); + return PacketRegistry.getPreviousPackets().containsKey(packetID); } + @Override public Set getPacketHandlers() { - return MinecraftRegistry.getPreviousPackets().keySet(); + return PacketRegistry.getPreviousPackets().keySet(); } // Called from the ReadPacketModified monitor - PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) { + public PacketEvent packetRecieved(PacketContainer packet, DataInputStream input) { try { Player client = playerInjection.getPlayerByConnection(input); @@ -225,18 +231,19 @@ class PacketInjector { * @param client - the client that sent the packet. * @return The resulting packet event. */ + @Override public PacketEvent packetRecieved(PacketContainer packet, Player client) { - PacketEvent event = PacketEvent.fromClient((Object) manager, packet, client); manager.invokePacketRecieving(event); return event; } + @Override @SuppressWarnings("rawtypes") public synchronized void cleanupAll() { - Map overwritten = MinecraftRegistry.getOverwrittenPackets(); - Map previous = MinecraftRegistry.getPreviousPackets(); + Map overwritten = PacketRegistry.getOverwrittenPackets(); + Map previous = PacketRegistry.getPreviousPackets(); // Remove every packet handler for (Integer id : previous.keySet().toArray(new Integer[0])) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java similarity index 96% rename from ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java rename to ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java index ceb702c6..00eacf9c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/ReadPacketModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ReadPacketModifier.java @@ -15,7 +15,7 @@ * 02111-1307 USA */ -package com.comphenix.protocol.injector; +package com.comphenix.protocol.injector.packet; import java.io.DataInputStream; import java.lang.reflect.Method; @@ -41,7 +41,7 @@ class ReadPacketModifier implements MethodInterceptor { private static final Object CANCEL_MARKER = new Object(); // Common for all packets of the same type - private PacketInjector packetInjector; + private ProxyPacketInjector packetInjector; private int packetID; // Report errors @@ -50,7 +50,7 @@ class ReadPacketModifier implements MethodInterceptor { // Whether or not a packet has been cancelled private static Map override = Collections.synchronizedMap(new WeakHashMap()); - public ReadPacketModifier(int packetID, PacketInjector packetInjector, ErrorReporter reporter) { + public ReadPacketModifier(int packetID, ProxyPacketInjector packetInjector, ErrorReporter reporter) { this.packetID = packetID; this.packetInjector = packetInjector; this.reporter = reporter; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java index 60f570b5..57bf8e37 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/InjectedArrayList.java @@ -22,6 +22,8 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Set; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.player.NetworkFieldInjector.FakePacket; @@ -64,7 +66,8 @@ class InjectedArrayList extends ArrayList { if (packet instanceof FakePacket) { return true; } else if (ignoredPackets.contains(packet)) { - ignoredPackets.remove(packet); + // Don't send it to the filters + result = ignoredPackets.remove(packet); } else { result = injector.handlePacketSending(packet); } @@ -82,7 +85,18 @@ class InjectedArrayList extends ArrayList { return true; } catch (InvocationTargetException e) { - throw new RuntimeException("Reverting cancelled packet failed.", e.getTargetException()); + ErrorReporter reporter = ProtocolLibrary.getErrorReporter(); + + // Prefer to report this to the user, instead of risking sending it to Minecraft + if (reporter != null) { + reporter.reportDetailed(this, "Reverting cancelled packet failed.", e, packet); + } else { + System.out.println("[ProtocolLib] Reverting cancelled packet failed."); + e.printStackTrace(); + } + + // Failure + return false; } } 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 6123ec0f..c6860ab6 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 @@ -90,7 +90,8 @@ class InjectedServerConnection { try { if (serverConnectionMethod == null) serverConnectionMethod = FuzzyReflection.fromClass(minecraftServerField.getType()). - getMethodByParameters("getServerConnection", ".*ServerConnection", new String[] {}); + getMethodByParameters("getServerConnection", + MinecraftReflection.getServerConnectionClass(), new Class[] {}); // We're using Minecraft 1.3.1 injectServerConnection(); @@ -106,12 +107,10 @@ class InjectedServerConnection { } private void injectListenerThread() { - try { - - if (listenerThreadField == null) - listenerThreadField = FuzzyReflection.fromObject(minecraftServer). - getFieldByType(".*NetworkListenThread"); + if (listenerThreadField == null) + listenerThreadField = FuzzyReflection.fromObject(minecraftServer). + getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass()); } catch (RuntimeException e) { reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer); return; 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 dd03ea5d..f80bae7b 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 @@ -38,7 +38,7 @@ class NetLoginInjector { private ConcurrentMap injectedLogins = Maps.newConcurrentMap(); // Handles every hook - private PlayerInjectionHandler injectionHandler; + private ProxyPlayerInjectionHandler injectionHandler; private Server server; // The current error rerporter @@ -47,7 +47,7 @@ class NetLoginInjector { // Used to create fake players private TemporaryPlayerFactory tempPlayerFactory = new TemporaryPlayerFactory(); - public NetLoginInjector(ErrorReporter reporter, PlayerInjectionHandler injectionHandler, Server server) { + public NetLoginInjector(ErrorReporter reporter, ProxyPlayerInjectionHandler injectionHandler, Server server) { this.reporter = reporter; this.injectionHandler = injectionHandler; this.server = server; diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java index ffb6ca3d..88d9beec 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/NetworkFieldInjector.java @@ -28,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.bukkit.entity.Player; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketListener; 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 951c4b23..8fa193a4 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 @@ -28,22 +28,25 @@ import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; +import org.bukkit.Server; import org.bukkit.entity.Player; import com.comphenix.protocol.Packets; +import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.ListeningWhitelist; 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; /** - * Injection method that overrides the NetworkHandler itself, and it's sendPacket-method. + * Injection method that overrides the NetworkHandler itself, and it's queue-method. * * @author Kristian */ -class NetworkObjectInjector extends PlayerInjector { +public class NetworkObjectInjector extends PlayerInjector { // Determine if we're listening private IntegerSet sendingFilters; @@ -53,6 +56,9 @@ class NetworkObjectInjector extends PlayerInjector { // Shared callback filter - avoid creating a new class every time private static CallbackFilter callbackFilter; + // Temporary player factory + private static volatile TemporaryPlayerFactory tempPlayerFactory; + public NetworkObjectInjector(ClassLoader classLoader, ErrorReporter reporter, Player player, ListenerInvoker invoker, IntegerSet sendingFilters) throws IllegalAccessException { super(reporter, player, invoker); @@ -65,6 +71,21 @@ class NetworkObjectInjector extends PlayerInjector { return sendingFilters.contains(packetID); } + /** + * Create a temporary player for use during login. + * @param server - Bukkit server. + * @return The temporary player. + */ + public Player createTemporaryPlayer(Server server) { + 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; + } + @Override public void sendServerPacket(Object packet, boolean filtered) throws InvocationTargetException { Object networkDelegate = filtered ? networkManagerRef.getValue() : networkManagerRef.getOldValue(); 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 74a0a3e6..f81be360 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,7 +20,8 @@ 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; @@ -31,6 +32,7 @@ import net.sf.cglib.proxy.NoOp; import org.bukkit.entity.Player; +import com.comphenix.protocol.concurrency.IntegerSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.GamePhase; @@ -43,13 +45,14 @@ 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.MinecraftReflection; +import com.google.common.collect.Maps; /** * Represents a player hook into the NetServerHandler class. * * @author Kristian */ -public class NetworkServerInjector extends PlayerInjector { +class NetworkServerInjector extends PlayerInjector { private volatile static CallbackFilter callbackFilter; @@ -91,19 +94,69 @@ public class NetworkServerInjector extends PlayerInjector { // Get the send packet method! if (hasInitialized) { - if (sendPacketMethod == null) - sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*"); + 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 serverDeleage = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); + Object serverDelegate = filtered ? serverHandlerRef.getValue() : serverHandlerRef.getOldValue(); - if (serverDeleage != null) { + if (serverDelegate != null) { try { // Note that invocation target exception is a wrapper for a checked exception - sendPacketMethod.invoke(serverDeleage, packet); + sendPacketMethod.invoke(serverDelegate, packet); } catch (IllegalArgumentException e) { throw e; @@ -296,8 +349,22 @@ public class NetworkServerInjector extends PlayerInjector { } FieldUtils.writeField(disconnectField, handler, value); - } catch (IllegalArgumentException e) { - reporter.reportDetailed(this, "Unable to find disconnect field. Is ProtocolLib up to date?", e, handler); + } catch (IllegalArgumentException e) { + // Assume it's the first ... + if (disconnectField == null) { + disconnectField = FuzzyReflection.fromObject(handler).getFieldByType("disconnected", boolean.class); + reporter.reportWarning(this, "Unable to find 'disconnected' field. Assuming " + disconnectField); + + // Try again + if (disconnectField != null) { + setDisconnect(handler, value); + return; + } + } + + // This is really bad + reporter.reportDetailed(this, "Cannot find disconnected field. Is ProtocolLib up to date?", e); + } catch (IllegalAccessException e) { reporter.reportWarning(this, "Unable to update disconnected field. Player quit event may be sent twice."); } 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 a59e5772..a1fe1b44 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 @@ -1,210 +1,66 @@ -/* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - 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 org.bukkit.Server; import org.bukkit.entity.Player; -import com.comphenix.protocol.concurrency.BlockingHashMap; -import com.comphenix.protocol.error.ErrorReporter; -import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; 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.google.common.base.Predicate; -import com.google.common.collect.Maps; - -/** - * Responsible for injecting into a player's sendPacket method. - * - * @author Kristian - */ -public class PlayerInjectionHandler { - /** - * The maximum number of milliseconds to wait until a player can be looked up by connection. - */ - private static final long TIMEOUT_PLAYER_LOOKUP = 2000; // ms - - /** - * The highest possible packet ID. It's unlikely that this value will ever change. - */ - private static final int MAXIMUM_PACKET_ID = 255; - - // Server connection injection - private InjectedServerConnection serverInjection; - - // 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(); - - // Lookup player by connection - private BlockingHashMap dataInputLookup = BlockingHashMap.create(); - - // Player injection types - private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; - private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; - - // Error reporter - private ErrorReporter reporter; - - // Whether or not we're closing - private boolean hasClosed; - - // Used to invoke events - private ListenerInvoker invoker; - - // Enabled packet filters - private IntegerSet sendingFilters = new IntegerSet(MAXIMUM_PACKET_ID + 1); - - // List of packet listeners - private Set packetListeners; - - // The class loader we're using - private ClassLoader classLoader; - - // Used to filter injection attempts - private Predicate injectionFilter; - - public PlayerInjectionHandler(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); - serverInjection.injectList(); - } +public interface PlayerInjectionHandler { /** * Retrieves how the server packets are read. * @return Injection method for reading server packets. */ - public PlayerInjectHooks getPlayerHook() { - return getPlayerHook(GamePhase.PLAYING); - } - + public abstract PlayerInjectHooks getPlayerHook(); + /** * Retrieves how the server packets are read. * @param phase - the current game phase. * @return Injection method for reading server packets. */ - public PlayerInjectHooks getPlayerHook(GamePhase phase) { - switch (phase) { - case LOGIN: - return loginPlayerHook; - case PLAYING: - return playingPlayerHook; - default: - throw new IllegalArgumentException("Cannot retrieve injection hook for both phases at the same time."); - } - } + public abstract PlayerInjectHooks getPlayerHook(GamePhase phase); /** * Sets how the server packets are read. * @param playerHook - the new injection method for reading server packets. */ - public void setPlayerHook(PlayerInjectHooks playerHook) { - setPlayerHook(GamePhase.PLAYING, playerHook); - } - + public abstract void setPlayerHook(PlayerInjectHooks playerHook); + /** * Sets how the server packets are read. * @param phase - the current game phase. * @param playerHook - the new injection method for reading server packets. */ - public void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook) { - if (phase.hasLogin()) - loginPlayerHook = playerHook; - if (phase.hasPlaying()) - playingPlayerHook = playerHook; - - // Make sure the current listeners are compatible - checkListener(packetListeners); - } - + public abstract void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook); + /** * Add an underlying packet handler of the given ID. * @param packetID - packet ID to register. */ - public void addPacketHandler(int packetID) { - sendingFilters.add(packetID); - } - + public abstract void addPacketHandler(int packetID); + /** * Remove an underlying packet handler of ths ID. * @param packetID - packet ID to unregister. */ - public void removePacketHandler(int packetID) { - sendingFilters.remove(packetID); - } + public abstract void removePacketHandler(int packetID); - /** - * Used to construct a player hook. - * @param player - the player to hook. - * @param hook - the hook type. - * @return A new player hoook - * @throws IllegalAccessException Unable to do our reflection magic. - */ - private PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException { - // Construct the correct player hook - switch (hook) { - case NETWORK_HANDLER_FIELDS: - return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters); - case NETWORK_MANAGER_OBJECT: - return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters); - case NETWORK_SERVER_OBJECT: - return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection); - default: - throw new IllegalArgumentException("Cannot construct a player injector."); - } - } - /** * Retrieve a player by its DataInput connection. * @param inputStream - the associated DataInput connection. * @return The player. * @throws InterruptedException If the thread was interrupted during the wait. */ - public Player getPlayerByConnection(DataInputStream inputStream) throws InterruptedException { - return getPlayerByConnection(inputStream, TIMEOUT_PLAYER_LOOKUP, TimeUnit.MILLISECONDS); - } - + public abstract Player getPlayerByConnection(DataInputStream inputStream) + throws InterruptedException; + /** * Retrieve a player by its DataInput connection. * @param inputStream - the associated DataInput connection. @@ -213,243 +69,29 @@ public class PlayerInjectionHandler { * @return The player. * @throws InterruptedException If the thread was interrupted during the wait. */ - public Player getPlayerByConnection(DataInputStream inputStream, long playerTimeout, TimeUnit unit) throws InterruptedException { - // Wait until the connection owner has been established - PlayerInjector injector = dataInputLookup.get(inputStream, playerTimeout, unit); - - if (injector != null) { - return injector.getPlayer(); - } else { - reporter.reportWarning(this, "Unable to find stream: " + inputStream); - return null; - } - } - - /** - * Helper function that retrieves the injector type of a given player injector. - * @param injector - injector type. - * @return The injector type. - */ - private PlayerInjectHooks getInjectorType(PlayerInjector injector) { - return injector != null ? injector.getHookType() : PlayerInjectHooks.NONE; - } - + 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. */ - public void injectPlayer(Player player) { - // Inject using the player instance itself - if (isInjectionNecessary(GamePhase.PLAYING)) { - injectPlayer(player, player, GamePhase.PLAYING); - } - } - - /** - * Determine if it's truly necessary to perform the given player injection. - * @param phase - current game phase. - * @return TRUE if we should perform the injection, FALSE otherwise. - */ - public boolean isInjectionNecessary(GamePhase phase) { - return injectionFilter.apply(phase); - } - - /** - * Initialize a player hook, allowing us to read server packets. - *

- * This method will always perform the instructed injection. - * - * @param player - player to hook. - * @param injectionPoint - the object to use during the injection process. - * @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) { - // Unfortunately, due to NetLoginHandler, multiple threads may potentially call this method. - synchronized (player) { - return injectPlayerInternal(player, injectionPoint, phase); - } - } - - // Unsafe variant of the above - private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, GamePhase phase) { - - PlayerInjector injector = playerInjection.get(player); - PlayerInjectHooks tempHook = getPlayerHook(phase); - PlayerInjectHooks permanentHook = tempHook; - - // The given player object may be fake, so be careful! - - // See if we need to inject something else - boolean invalidInjector = injector != null ? !injector.canInject(phase) : true; + public abstract void injectPlayer(Player player); - // Don't inject if the class has closed - if (!hasClosed && player != null && (tempHook != getInjectorType(injector) || invalidInjector)) { - while (tempHook != PlayerInjectHooks.NONE) { - // Whether or not the current hook method failed completely - boolean hookFailed = false; - - // Remove the previous hook, if any - cleanupHook(injector); - - try { - injector = getHookInstance(player, tempHook); - - // Make sure this injection method supports the current game phase - if (injector.canInject(phase)) { - injector.initialize(injectionPoint); - - DataInputStream inputStream = injector.getInputStream(false); - - 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); - } - - injector.injectManager(); - - if (inputStream != null) - dataInputLookup.put(inputStream, injector); - if (address != null) - addressLookup.put(address, injector); - break; - } - - } catch (PlayerLoggedOutException e) { - throw e; - - } catch (Exception e) { - // Mark this injection attempt as a failure - reporter.reportDetailed(this, "Player hook " + tempHook.toString() + " failed.", - e, player, injectionPoint, phase); - hookFailed = true; - } - - // Choose the previous player hook type - tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1]; - - if (hookFailed) - reporter.reportWarning(this, "Switching to " + tempHook.toString() + " instead."); - - // Check for UTTER FAILURE - if (tempHook == PlayerInjectHooks.NONE) { - cleanupHook(injector); - injector = null; - hookFailed = true; - } - - // Should we set the default hook method too? - if (hookFailed) { - permanentHook = tempHook; - } - } - - // Update values - if (injector != null) - lastSuccessfulHook = injector; - if (permanentHook != getPlayerHook(phase)) - setPlayerHook(phase, tempHook); - - // Save injector - if (injector != null) { - playerInjection.put(player, injector); - } - } - - return injector; - } - - private void cleanupHook(PlayerInjector injector) { - // Clean up as much as possible - try { - if (injector != null) - injector.cleanupAll(); - } catch (Exception ex) { - reporter.reportDetailed(this, "Cleaing up after player hook failed.", ex, injector); - } - } - /** * Invoke special routines for handling disconnect before a player is uninjected. * @param player - player to process. */ - public void handleDisconnect(Player player) { - PlayerInjector injector = getInjector(player); - - if (injector != null) { - injector.handleDisconnect(); - } - } - - /** - * Unregisters the given player. - * @param player - player to unregister. - * @return TRUE if a player has been uninjected, FALSE otherwise. - */ - public boolean uninjectPlayer(Player player) { - return uninjectPlayer(player, true, 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) { - if (!hasClosed && player != null) { - - PlayerInjector injector = playerInjection.remove(player); + public abstract void handleDisconnect(Player player); - if (injector != null) { - InetSocketAddress address = player.getAddress(); - injector.cleanupAll(); - - // Remove the "hooked" network manager in our instance as well - if (prepareNextHook && injector instanceof NetworkObjectInjector) { - try { - PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); - dummyInjector.initializePlayer(player); - dummyInjector.setNetworkManager(injector.getNetworkManager(), true); + /** + * Unregisters the given player. + * @param player - player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + public abstract boolean uninjectPlayer(Player player); - } catch (IllegalAccessException e) { - // Let the user know - reporter.reportWarning(this, "Unable to fully revert old injector. May cause conflicts.", e); - } - } - - // Clean up - if (removeAuxiliary) { - // Note that the dataInputLookup will clean itself - if (address != null) - addressLookup.remove(address); - } - return true; - } - } - - return false; - } - /** * Unregisters a player by the given address. *

@@ -459,19 +101,8 @@ public class PlayerInjectionHandler { * @param address - address of the player to unregister. * @return TRUE if a player has been uninjected, FALSE otherwise. */ - public boolean uninjectPlayer(InetSocketAddress address) { - if (!hasClosed && address != null) { - PlayerInjector injector = addressLookup.get(address); - - // Clean up - if (injector != null) - uninjectPlayer(injector.getPlayer(), false, true); - return true; - } - - return false; - } - + public abstract boolean uninjectPlayer(InetSocketAddress address); + /** * Send the given packet to the given reciever. * @param reciever - the player receiver. @@ -479,19 +110,9 @@ public class PlayerInjectionHandler { * @param filters - whether or not to invoke the packet filters. * @throws InvocationTargetException If an error occured during sending. */ - public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { - PlayerInjector injector = getInjector(reciever); - - // Send the packet, or drop it completely - if (injector != null) - injector.sendServerPacket(packet.getHandle(), filters); - else - throw new PlayerLoggedOutException(String.format( - "Unable to send packet %s (%s): Player %s has logged out.", - packet.getID(), packet, reciever.getName() - )); - } - + public abstract void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) + throws InvocationTargetException; + /** * Process a packet as if it were sent by the given player. * @param player - the sender. @@ -499,133 +120,37 @@ public class PlayerInjectionHandler { * @throws IllegalAccessException If the reflection machinery failed. * @throws InvocationTargetException If the underlying method caused an error. */ - public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { - - PlayerInjector injector = getInjector(player); - - // Process the given packet, or simply give up - if (injector != null) - injector.processPacket(mcPacket); - else - throw new PlayerLoggedOutException(String.format( - "Unable to receieve packet %s. Player %s has logged out.", - mcPacket, player.getName() - )); - } - - /** - * Retrieve the injector associated with this player. - * @param player - the player to find. - * @return The injector, or NULL if not found. - */ - private PlayerInjector getInjector(Player player) { - return playerInjection.get(player); - } - - /** - * Retrieve a player injector by looking for its NetworkManager. - * @param networkManager - current network manager. - * @return Related player injector. - */ - PlayerInjector getInjectorByNetworkHandler(Object networkManager) { - // That's not legal - if (networkManager == null) - return null; - - // O(n) is okay in this instance. This is only a backup solution. - for (PlayerInjector injector : playerInjection.values()) { - if (injector.getNetworkManager() == networkManager) - return injector; - } - - // None found - return null; - } - + public abstract void processPacket(Player player, Object mcPacket) + throws IllegalAccessException, InvocationTargetException; + /** * Determine if the given listeners are valid. * @param listeners - listeners to check. */ - public void checkListener(Set listeners) { - // Make sure the current listeners are compatible - if (lastSuccessfulHook != null) { - for (PacketListener listener : listeners) { - checkListener(listener); - } - } - } - + public abstract void checkListener(Set listeners); + /** * Determine if a listener is valid or not. *

* If not, a warning will be printed to the console. * @param listener - listener to check. */ - public void checkListener(PacketListener listener) { - if (lastSuccessfulHook != null) { - UnsupportedListener result = lastSuccessfulHook.checkListener(listener); + public abstract void checkListener(PacketListener listener); - // We won't prevent the listener, as it may still have valid packets - if (result != null) { - reporter.reportWarning(this, "Cannot fully register listener for " + - PacketAdapter.getPluginName(listener) + ": " + result.toString()); - - // These are illegal - for (int packetID : result.getPackets()) - removePacketHandler(packetID); - } - } - } - /** * Retrieve the current list of registered sending listeners. * @return List of the sending listeners's packet IDs. */ - public Set getSendingFilters() { - return sendingFilters.toSet(); - } - - public void close() { - // Guard - if (hasClosed || playerInjection == null) - return; + public abstract Set getSendingFilters(); - // Remove everything - for (PlayerInjector injection : playerInjection.values()) { - if (injection != null) { - injection.cleanupAll(); - } - } - - // Remove server handler - if (serverInjection != null) - serverInjection.cleanupAll(); - if (netLoginInjector != null) - netLoginInjector.cleanupAll(); - serverInjection = null; - netLoginInjector = null; - hasClosed = true; - - playerInjection.clear(); - addressLookup.clear(); - invoker = null; - } + /** + * Close any lingering proxy injections. + */ + public abstract void close(); /** * Inform the current PlayerInjector that it should update the DataInputStream next. * @param player - the player to update. */ - 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); - } - }); - } - } -} + public abstract void scheduleDataInputRefresh(Player player); +} \ 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 25d683c6..8f84cdd0 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 @@ -58,10 +58,10 @@ abstract class PlayerInjector { protected static Field proxyServerField; protected static Field networkManagerField; - protected static Field inputField; protected static Field netHandlerField; protected static Field socketField; + private static Field inputField; private static Field entityPlayerField; // Whether or not we're using a proxy type @@ -135,7 +135,7 @@ abstract class PlayerInjector { //Dispatch to the correct injection method if (injectionSource instanceof Player) - initializePlayer(injectionSource); + initializePlayer((Player) injectionSource); else if (MinecraftReflection.isLoginHandler(injectionSource)) initializeLogin(injectionSource); else @@ -146,10 +146,12 @@ abstract class PlayerInjector { * Initialize the player injector using an actual player instance. * @param player - the player to hook. */ - public void initializePlayer(Object player) { - + public void initializePlayer(Player player) { Object notchEntity = getEntityPlayer((Player) player); + // Save the player too + this.player = player; + if (!hasInitialized) { // Do this first, in case we encounter an exception hasInitialized = true; @@ -167,24 +169,29 @@ abstract class PlayerInjector { // Next, get the network manager if (networkManagerField == null) - networkManagerField = FuzzyReflection.fromObject(serverHandler). - getFieldByType(".*" + MinecraftReflection.getNetworkManagerName()); + networkManagerField = FuzzyReflection.fromObject(serverHandler).getFieldByType( + "networkManager", MinecraftReflection.getNetworkManagerClass()); initializeNetworkManager(networkManagerField, serverHandler); } } /** - * Initialize the player injector for a NetLoginHandler instead. + * Initialize the player injector from a NetLoginHandler. * @param netLoginHandler - the net login handler to inject. */ public void initializeLogin(Object netLoginHandler) { if (!hasInitialized) { + // Just in case + if (!MinecraftReflection.isLoginHandler(netLoginHandler)) + throw new IllegalArgumentException("netLoginHandler (" + netLoginHandler + ") is not a " + + MinecraftReflection.getNetLoginHandlerName()); + hasInitialized = true; loginHandler = netLoginHandler; if (netLoginNetworkField == null) netLoginNetworkField = FuzzyReflection.fromObject(netLoginHandler). - getFieldByType(".*" + MinecraftReflection.getNetworkManagerName()); + getFieldByType("networkManager", MinecraftReflection.getNetworkManagerClass()); initializeNetworkManager(netLoginNetworkField, netLoginHandler); } } @@ -206,11 +213,6 @@ abstract class PlayerInjector { if (queueMethod == null) queueMethod = FuzzyReflection.fromClass(reference.getType()). getMethodByParameters("queue", MinecraftReflection.getPacketClass()); - - // And the data input stream that we'll use to identify a player - if (inputField == null) - inputField = FuzzyReflection.fromObject(networkManager, true). - getFieldByType("java\\.io\\.DataInputStream"); } /** @@ -250,9 +252,9 @@ abstract class PlayerInjector { public Socket getSocket() throws IllegalAccessException { try { if (socketField == null) - socketField = FuzzyReflection.fromObject(networkManager).getFieldListByType(Socket.class).get(0); + socketField = FuzzyReflection.fromObject(networkManager, true).getFieldListByType(Socket.class).get(0); if (socket == null) - socket = (Socket) FieldUtils.readField(socketField, networkManager); + socket = (Socket) FieldUtils.readField(socketField, networkManager, true); return socket; } catch (IndexOutOfBoundsException e) { @@ -290,7 +292,13 @@ abstract class PlayerInjector { // Execute disconnect on it if (handler != null) { if (disconnect == null) { - disconnect = FuzzyReflection.fromObject(handler).getMethodByName("disconnect.*"); + try { + disconnect = FuzzyReflection.fromObject(handler).getMethodByName("disconnect.*"); + } catch (IllegalArgumentException e) { + // Just assume it's the first String method + disconnect = FuzzyReflection.fromObject(handler).getMethodByParameters("disconnect", String.class); + reporter.reportWarning(this, "Cannot find disconnect method by name. Assuming " + disconnect); + } // Save the method for later if (usingNetServer) @@ -330,7 +338,7 @@ abstract class PlayerInjector { Object handler = FieldUtils.readField(serverHandlerField, notchEntity, true); // Is this a Minecraft hook? - if (handler != null && !handler.getClass().getName().startsWith("net.minecraft.server")) { + if (handler != null && !MinecraftReflection.isMinecraftObject(handler)) { // This is our proxy object if (handler instanceof Factory) @@ -380,7 +388,7 @@ abstract class PlayerInjector { try { // Well, that sucks. Try just Minecraft objects then. netHandlerField = FuzzyReflection.fromClass(networkManager.getClass(), true). - getFieldByType(MinecraftReflection.MINECRAFT_OBJECT); + getFieldByType(MinecraftReflection.getMinecraftObjectRegex()); } catch (RuntimeException e2) { throw new IllegalAccessException("Cannot locate net handler. " + e2.getMessage()); @@ -564,11 +572,13 @@ abstract class PlayerInjector { * @return The player's input stream. */ public DataInputStream getInputStream(boolean cache) { - if (inputField == null) - throw new IllegalStateException("Input field is NULL."); + // And the data input stream that we'll use to identify a player if (networkManager == null) - throw new IllegalStateException("Network manager is NULL."); - + throw new IllegalStateException("Network manager is NULL."); + if (inputField == null) + inputField = FuzzyReflection.fromObject(networkManager, true). + getFieldByType("java\\.io\\.DataInputStream"); + // Get the associated input stream try { if (cache && cachedInput != null) @@ -598,6 +608,16 @@ abstract class PlayerInjector { return player; } + /** + * Set the hooked player. + *

+ * Should only be called during the creation of the injector. + * @param player - the new hooked player. + */ + public void setPlayer(Player player) { + this.player = player; + } + /** * Object that can invoke the packet events. * @return Packet event invoker. @@ -616,4 +636,12 @@ abstract class PlayerInjector { else return player; } + + /** + * Set the real Bukkit player that we will use. + * @param updatedPlayer - the real Bukkit player. + */ + public void setUpdatedPlayer(Player updatedPlayer) { + this.updatedPlayer = updatedPlayer; + } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java new file mode 100644 index 00000000..120d5dc9 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/PlayerInjectorBuilder.java @@ -0,0 +1,145 @@ +package com.comphenix.protocol.injector.player; + +import java.util.Set; + +import javax.annotation.Nonnull; + +import org.bukkit.Bukkit; +import org.bukkit.Server; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PacketFilterManager; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; + +/** + * Constructor for different player injectors. + * + * @author Kristian + */ +public class PlayerInjectorBuilder { + public static PlayerInjectorBuilder newBuilder() { + return new PlayerInjectorBuilder(); + } + + protected PlayerInjectorBuilder() { + // Use the static method. + } + + protected ClassLoader classLoader; + protected ErrorReporter reporter; + protected Predicate injectionFilter; + protected ListenerInvoker invoker; + protected Set packetListeners; + protected Server server; + + /** + * Set the class loader to use during class generation. + * @param classLoader - new class loader. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder classLoader(@Nonnull ClassLoader classLoader) { + Preconditions.checkNotNull(classLoader, "classLoader cannot be NULL"); + this.classLoader = classLoader; + return this; + } + + /** + * The error reporter used by the created injector. + * @param reporter - new error reporter. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder reporter(@Nonnull ErrorReporter reporter) { + Preconditions.checkNotNull(reporter, "reporter cannot be NULL"); + this.reporter = reporter; + return this; + } + + /** + * The injection filter that is used to determine if it is necessary to perform + * injection during a certain phase. + * @param injectionFilter - filter predicate. + * @return This builder, for chaining. + */ + @Nonnull + public PlayerInjectorBuilder injectionFilter(@Nonnull Predicate injectionFilter) { + Preconditions.checkNotNull(injectionFilter, "injectionFilter cannot be NULL"); + this.injectionFilter = injectionFilter; + return this; + } + + /** + * The packet stream invoker. + * @param invoker - the invoker. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder invoker(@Nonnull ListenerInvoker invoker) { + Preconditions.checkNotNull(invoker, "invoker cannot be NULL"); + this.invoker = invoker; + return this; + } + + /** + * Set the set of packet listeners. + * @param packetListeners - packet listeners. + * @return This builder, for chaining. + */ + @Nonnull + public PlayerInjectorBuilder packetListeners(@Nonnull Set packetListeners) { + Preconditions.checkNotNull(packetListeners, "packetListeners cannot be NULL"); + this.packetListeners = packetListeners; + return this; + } + + /** + * Set the Bukkit server used for scheduling. + * @param server - the Bukkit server. + * @return This builder, for chaining. + */ + public PlayerInjectorBuilder server(@Nonnull Server server) { + Preconditions.checkNotNull(server, "server cannot be NULL"); + this.server = server; + return this; + } + + /** + * Called before an object is created with this builder. + */ + private void initializeDefaults() { + ProtocolManager manager = ProtocolLibrary.getProtocolManager(); + + // Initialize with default values if we can + if (classLoader == null) + classLoader = this.getClass().getClassLoader(); + if (reporter == null) + reporter = ProtocolLibrary.getErrorReporter(); + if (invoker == null) + invoker = (PacketFilterManager) manager; + if (server == null) + server = Bukkit.getServer(); + if (injectionFilter == null) + throw new IllegalStateException("injectionFilter must be initialized."); + if (packetListeners == null) + throw new IllegalStateException("packetListeners must be initialized."); + } + + /** + * Construct the injection handler. + *

+ * Any builder parameter marked as NON-NULL is essential and must be initialized. + * @return The constructed injection handler using the current parameters. + */ + public PlayerInjectionHandler buildHandler() { + // Fill any default fields + initializeDefaults(); + + return new ProxyPlayerInjectionHandler( + classLoader, reporter, injectionFilter, + invoker, packetListeners, server); + } +} 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 new file mode 100644 index 00000000..053fe0ef --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/ProxyPlayerInjectionHandler.java @@ -0,0 +1,674 @@ +/* + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2012 Kristian S. Stangeland + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program; + * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA + * 02111-1307 USA + */ + +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 org.bukkit.Server; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.concurrency.BlockingHashMap; +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketListener; +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.google.common.base.Predicate; +import com.google.common.collect.Maps; + +/** + * Responsible for injecting into a player's sendPacket method. + * + * @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; + + // 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(); + + // Lookup player by connection + private BlockingHashMap dataInputLookup = BlockingHashMap.create(); + + // Player injection types + private volatile PlayerInjectHooks loginPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; + private volatile PlayerInjectHooks playingPlayerHook = PlayerInjectHooks.NETWORK_SERVER_OBJECT; + + // Error reporter + private ErrorReporter reporter; + + // Whether or not we're closing + private boolean hasClosed; + + // Used to invoke events + private ListenerInvoker invoker; + + // Enabled packet filters + private IntegerSet sendingFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + + // List of packet listeners + private Set packetListeners; + + // The class loader we're using + private ClassLoader classLoader; + + // Used to filter injection attempts + private Predicate injectionFilter; + + 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); + serverInjection.injectList(); + } + + /** + * Retrieves how the server packets are read. + * @return Injection method for reading server packets. + */ + @Override + public PlayerInjectHooks getPlayerHook() { + return getPlayerHook(GamePhase.PLAYING); + } + + /** + * Retrieves how the server packets are read. + * @param phase - the current game phase. + * @return Injection method for reading server packets. + */ + @Override + public PlayerInjectHooks getPlayerHook(GamePhase phase) { + switch (phase) { + case LOGIN: + return loginPlayerHook; + case PLAYING: + return playingPlayerHook; + default: + throw new IllegalArgumentException("Cannot retrieve injection hook for both phases at the same time."); + } + } + + /** + * Sets how the server packets are read. + * @param playerHook - the new injection method for reading server packets. + */ + @Override + public void setPlayerHook(PlayerInjectHooks playerHook) { + setPlayerHook(GamePhase.PLAYING, playerHook); + } + + /** + * Sets how the server packets are read. + * @param phase - the current game phase. + * @param playerHook - the new injection method for reading server packets. + */ + @Override + public void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook) { + if (phase.hasLogin()) + loginPlayerHook = playerHook; + if (phase.hasPlaying()) + playingPlayerHook = playerHook; + + // Make sure the current listeners are compatible + checkListener(packetListeners); + } + + /** + * Add an underlying packet handler of the given ID. + * @param packetID - packet ID to register. + */ + @Override + public void addPacketHandler(int packetID) { + sendingFilters.add(packetID); + } + + /** + * Remove an underlying packet handler of ths ID. + * @param packetID - packet ID to unregister. + */ + @Override + public void removePacketHandler(int packetID) { + sendingFilters.remove(packetID); + } + + /** + * Used to construct a player hook. + * @param player - the player to hook. + * @param hook - the hook type. + * @return A new player hoook + * @throws IllegalAccessException Unable to do our reflection magic. + */ + private PlayerInjector getHookInstance(Player player, PlayerInjectHooks hook) throws IllegalAccessException { + // Construct the correct player hook + switch (hook) { + case NETWORK_HANDLER_FIELDS: + return new NetworkFieldInjector(classLoader, reporter, player, invoker, sendingFilters); + case NETWORK_MANAGER_OBJECT: + return new NetworkObjectInjector(classLoader, reporter, player, invoker, sendingFilters); + case NETWORK_SERVER_OBJECT: + return new NetworkServerInjector(classLoader, reporter, player, invoker, sendingFilters, serverInjection); + default: + throw new IllegalArgumentException("Cannot construct a player injector."); + } + } + + /** + * 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. + */ + @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 { + // Wait until the connection owner has been established + PlayerInjector injector = dataInputLookup.get(inputStream, playerTimeout, unit); + + if (injector != null) { + return injector.getPlayer(); + } else { + reporter.reportWarning(this, "Unable to find stream: " + inputStream); + return null; + } + } + + /** + * Helper function that retrieves the injector type of a given player injector. + * @param injector - injector type. + * @return The injector type. + */ + private PlayerInjectHooks getInjectorType(PlayerInjector injector) { + return injector != null ? injector.getHookType() : PlayerInjectHooks.NONE; + } + + /** + * 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. + */ + @Override + public void injectPlayer(Player player) { + // Inject using the player instance itself + if (isInjectionNecessary(GamePhase.PLAYING)) { + injectPlayer(player, player, GamePhase.PLAYING); + } + } + + /** + * Determine if it's truly necessary to perform the given player injection. + * @param phase - current game phase. + * @return TRUE if we should perform the injection, FALSE otherwise. + */ + public boolean isInjectionNecessary(GamePhase phase) { + return injectionFilter.apply(phase); + } + + /** + * Initialize a player hook, allowing us to read server packets. + *

+ * This method will always perform the instructed injection. + * + * @param player - player to hook. + * @param injectionPoint - the object to use during the injection process. + * @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) { + // Unfortunately, due to NetLoginHandler, multiple threads may potentially call this method. + synchronized (player) { + return injectPlayerInternal(player, injectionPoint, phase); + } + } + + // Unsafe variant of the above + private PlayerInjector injectPlayerInternal(Player player, Object injectionPoint, GamePhase phase) { + + PlayerInjector injector = playerInjection.get(player); + PlayerInjectHooks tempHook = getPlayerHook(phase); + PlayerInjectHooks permanentHook = tempHook; + + // The given player object may be fake, so be careful! + + // See if we need to inject something else + boolean invalidInjector = injector != null ? !injector.canInject(phase) : true; + + // Don't inject if the class has closed + if (!hasClosed && player != null && (tempHook != getInjectorType(injector) || invalidInjector)) { + while (tempHook != PlayerInjectHooks.NONE) { + // Whether or not the current hook method failed completely + boolean hookFailed = false; + + // Remove the previous hook, if any + cleanupHook(injector); + + try { + injector = getHookInstance(player, tempHook); + + // Make sure this injection method supports the current game phase + if (injector.canInject(phase)) { + injector.initialize(injectionPoint); + + DataInputStream inputStream = injector.getInputStream(false); + + 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); + } + + injector.injectManager(); + + if (inputStream != null) + dataInputLookup.put(inputStream, injector); + if (address != null) + addressLookup.put(address, injector); + break; + } + + } catch (PlayerLoggedOutException e) { + throw e; + + } catch (Exception e) { + // Mark this injection attempt as a failure + reporter.reportDetailed(this, "Player hook " + tempHook.toString() + " failed.", + e, player, injectionPoint, phase); + hookFailed = true; + } + + // Choose the previous player hook type + tempHook = PlayerInjectHooks.values()[tempHook.ordinal() - 1]; + + if (hookFailed) + reporter.reportWarning(this, "Switching to " + tempHook.toString() + " instead."); + + // Check for UTTER FAILURE + if (tempHook == PlayerInjectHooks.NONE) { + cleanupHook(injector); + injector = null; + hookFailed = true; + } + + // Should we set the default hook method too? + if (hookFailed) { + permanentHook = tempHook; + } + } + + // Update values + if (injector != null) + lastSuccessfulHook = injector; + if (permanentHook != getPlayerHook(phase)) + setPlayerHook(phase, tempHook); + + // Save injector + if (injector != null) { + playerInjection.put(player, injector); + } + } + + return injector; + } + + private void cleanupHook(PlayerInjector injector) { + // Clean up as much as possible + try { + if (injector != null) + injector.cleanupAll(); + } catch (Exception ex) { + reporter.reportDetailed(this, "Cleaing up after player hook failed.", ex, injector); + } + } + + /** + * Invoke special routines for handling disconnect before a player is uninjected. + * @param player - player to process. + */ + @Override + public void handleDisconnect(Player player) { + PlayerInjector injector = getInjector(player); + + if (injector != null) { + injector.handleDisconnect(); + } + } + + /** + * Unregisters the given player. + * @param player - player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + @Override + public boolean uninjectPlayer(Player player) { + return uninjectPlayer(player, true, 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) { + 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 + if (prepareNextHook && injector instanceof NetworkObjectInjector) { + try { + PlayerInjector dummyInjector = getHookInstance(player, PlayerInjectHooks.NETWORK_SERVER_OBJECT); + dummyInjector.initializePlayer(player); + dummyInjector.setNetworkManager(injector.getNetworkManager(), true); + + } catch (IllegalAccessException e) { + // Let the user know + reporter.reportWarning(this, "Unable to fully revert old injector. May cause conflicts.", e); + } + } + + // Clean up + if (removeAuxiliary) { + // Note that the dataInputLookup will clean itself + if (address != null) + addressLookup.remove(address); + } + return true; + } + } + + return false; + } + + /** + * Unregisters a player by the given address. + *

+ * If the server handler has been created before we've gotten a chance to unject the player, + * the method will try a workaround to remove the injected hook in the NetServerHandler. + * + * @param address - address of the player to unregister. + * @return TRUE if a player has been uninjected, FALSE otherwise. + */ + @Override + public boolean uninjectPlayer(InetSocketAddress address) { + if (!hasClosed && address != null) { + PlayerInjector injector = addressLookup.get(address); + + // Clean up + if (injector != null) + uninjectPlayer(injector.getPlayer(), false, true); + return true; + } + + return false; + } + + /** + * Send the given packet to the given reciever. + * @param reciever - the player receiver. + * @param packet - the packet to send. + * @param filters - whether or not to invoke the packet filters. + * @throws InvocationTargetException If an error occured during sending. + */ + @Override + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { + PlayerInjector injector = getInjector(reciever); + + // Send the packet, or drop it completely + if (injector != null) { + injector.sendServerPacket(packet.getHandle(), filters); + } else { + throw new PlayerLoggedOutException(String.format( + "Unable to send packet %s (%s): Player %s has logged out.", + packet.getID(), packet, reciever.getName() + )); + } + } + + /** + * Process a packet as if it were sent by the given player. + * @param player - the sender. + * @param mcPacket - the packet to process. + * @throws IllegalAccessException If the reflection machinery failed. + * @throws InvocationTargetException If the underlying method caused an error. + */ + @Override + public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + + PlayerInjector injector = getInjector(player); + + // Process the given packet, or simply give up + if (injector != null) + injector.processPacket(mcPacket); + else + throw new PlayerLoggedOutException(String.format( + "Unable to receieve packet %s. Player %s has logged out.", + mcPacket, player.getName() + )); + } + + /** + * Retrieve the injector associated with this player. + * @param player - the player to find. + * @return The injector, or NULL if not found. + */ + private PlayerInjector getInjector(Player player) { + PlayerInjector injector = playerInjection.get(player); + + if (injector == null) { + // Try getting it from the player itself + if (player instanceof InjectContainer) + return ((InjectContainer) player).getInjector(); + else + return searchAddressLookup(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. + */ + private PlayerInjector searchAddressLookup(Player player) { + // See if we can find it anywhere + for (PlayerInjector injector : addressLookup.values()) { + if (player.equals(injector.getUpdatedPlayer())) { + return injector; + } + } + return null; + } + + /** + * Retrieve a player injector by looking for its NetworkManager. + * @param networkManager - current network manager. + * @return Related player injector. + */ + PlayerInjector getInjectorByNetworkHandler(Object networkManager) { + // That's not legal + if (networkManager == null) + return null; + + // O(n) is okay in this instance. This is only a backup solution. + for (PlayerInjector injector : playerInjection.values()) { + if (injector.getNetworkManager() == networkManager) + return injector; + } + + // None found + return null; + } + + /** + * Determine if the given listeners are valid. + * @param listeners - listeners to check. + */ + @Override + public void checkListener(Set listeners) { + // Make sure the current listeners are compatible + if (lastSuccessfulHook != null) { + for (PacketListener listener : listeners) { + checkListener(listener); + } + } + } + + /** + * Determine if a listener is valid or not. + *

+ * If not, a warning will be printed to the console. + * @param listener - listener to check. + */ + @Override + public void checkListener(PacketListener listener) { + if (lastSuccessfulHook != null) { + UnsupportedListener result = lastSuccessfulHook.checkListener(listener); + + // We won't prevent the listener, as it may still have valid packets + if (result != null) { + reporter.reportWarning(this, "Cannot fully register listener for " + + PacketAdapter.getPluginName(listener) + ": " + result.toString()); + + // These are illegal + for (int packetID : result.getPackets()) + removePacketHandler(packetID); + } + } + } + + /** + * Retrieve the current list of registered sending listeners. + * @return List of the sending listeners's packet IDs. + */ + @Override + public Set getSendingFilters() { + return sendingFilters.toSet(); + } + + @Override + public void close() { + // Guard + if (hasClosed || playerInjection == null) + return; + + // Remove everything + for (PlayerInjector injection : playerInjection.values()) { + if (injection != null) { + injection.cleanupAll(); + } + } + + // Remove server handler + if (serverInjection != null) + serverInjection.cleanupAll(); + if (netLoginInjector != null) + netLoginInjector.cleanupAll(); + 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/player/TemporaryPlayerFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java index 45942257..67336df5 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/player/TemporaryPlayerFactory.java @@ -81,7 +81,7 @@ class TemporaryPlayerFactory { * *

* Note that the player a player has not been assigned a name yet, and thus cannot be - * uniquely identified. Use the + * uniquely identified. Use the address instead. * @param injector - the player injector used. * @param server - the current server. * @return A temporary player instance. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java new file mode 100644 index 00000000..e77a32de --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPacketInjector.java @@ -0,0 +1,57 @@ +package com.comphenix.protocol.injector.spigot; + +import java.util.Set; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.packet.PacketInjector; + +public class DummyPacketInjector implements PacketInjector { + private SpigotPacketInjector injector; + private IntegerSet reveivedFilters; + + public DummyPacketInjector(SpigotPacketInjector injector, IntegerSet reveivedFilters) { + this.injector = injector; + this.reveivedFilters = reveivedFilters; + } + + @Override + public void undoCancel(Integer id, Object packet) { + // Do nothing yet + } + + @Override + public boolean addPacketHandler(int packetID) { + reveivedFilters.add(packetID); + return true; + } + + @Override + public boolean removePacketHandler(int packetID) { + reveivedFilters.remove(packetID); + return true; + } + + @Override + public boolean hasPacketHandler(int packetID) { + return reveivedFilters.contains(packetID); + } + + @Override + public Set getPacketHandlers() { + return reveivedFilters.toSet(); + } + + @Override + public PacketEvent packetRecieved(PacketContainer packet, Player client) { + return injector.packetReceived(packet, client); + } + + @Override + public void cleanupAll() { + reveivedFilters.clear(); + } +} 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 new file mode 100644 index 00000000..ee8139f0 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/DummyPlayerHandler.java @@ -0,0 +1,123 @@ +package com.comphenix.protocol.injector.spigot; + +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; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketListener; +import com.comphenix.protocol.injector.GamePhase; +import com.comphenix.protocol.injector.PacketFilterManager.PlayerInjectHooks; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler; + +class DummyPlayerHandler implements PlayerInjectionHandler { + private SpigotPacketInjector injector; + private IntegerSet sendingFilters; + + public DummyPlayerHandler(SpigotPacketInjector injector, IntegerSet sendingFilters) { + this.injector = injector; + this.sendingFilters = sendingFilters; + } + + @Override + public boolean uninjectPlayer(InetSocketAddress address) { + return true; + } + + @Override + public boolean uninjectPlayer(Player player) { + injector.uninjectPlayer(player); + return true; + } + + @Override + public void setPlayerHook(GamePhase phase, PlayerInjectHooks playerHook) { + throw new UnsupportedOperationException("This is not needed in Spigot."); + } + + @Override + 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); + } + + @Override + public void removePacketHandler(int packetID) { + sendingFilters.remove(packetID); + } + + @Override + public Set getSendingFilters() { + return sendingFilters.toSet(); + } + + @Override + public void close() { + sendingFilters.clear(); + } + + @Override + public void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { + injector.sendServerPacket(reciever, packet, filters); + } + + @Override + public void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + injector.processPacket(player, mcPacket); + } + + @Override + public void injectPlayer(Player player) { + injector.injectPlayer(player); + } + + @Override + public void handleDisconnect(Player player) { + // Just ignore + } + + @Override + public PlayerInjectHooks getPlayerHook(GamePhase phase) { + return PlayerInjectHooks.NETWORK_SERVER_OBJECT; + } + + @Override + public PlayerInjectHooks getPlayerHook() { + // Pretend that we do + 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."); + } + + @Override + public void checkListener(PacketListener listener) { + // They're all fine! + } + + @Override + public void checkListener(Set listeners) { + // Yes, really + } +} 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 new file mode 100644 index 00000000..e52bd2e4 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -0,0 +1,466 @@ +package com.comphenix.protocol.injector.spigot; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; +import net.sf.cglib.proxy.NoOp; + +import com.comphenix.protocol.Packets; +import com.comphenix.protocol.concurrency.IntegerSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.injector.packet.PacketInjector; +import com.comphenix.protocol.injector.player.NetworkObjectInjector; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +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; +import com.google.common.collect.MapMaker; + +/** + * Offload all the work to Spigot, if possible. + * + * @author Kristian + */ +public class SpigotPacketInjector implements SpigotPacketListener { + // Lazily retrieve the spigot listener class + private static volatile Class spigotListenerClass; + private static volatile boolean classChecked; + + // Retrieve the entity player from a PlayerConnection + private static Field playerConnectionPlayer; + + // Packets that are not to be processed by the filters + private Set ignoredPackets = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); + + /** + * The amount of ticks to wait before removing all traces of a player. + */ + private static final int CLEANUP_DELAY = 100; + + /** + * Retrieve the spigot packet listener class. + * @return The listener class. + */ + private static Class getSpigotListenerClass() { + if (!classChecked) { + try { + spigotListenerClass = SpigotPacketInjector.class.getClassLoader().loadClass("org.spigotmc.netty.PacketListener"); + } catch (ClassNotFoundException e) { + return null; + } finally { + // We've given it a try now + classChecked = true; + } + } + return spigotListenerClass; + } + + /** + * Retrieve the register packet listener method. + * @return The method used to register a packet listener. + */ + private static Method getRegisterMethod() { + Class clazz = getSpigotListenerClass(); + + if (clazz != null) { + try { + return clazz.getMethod("register", clazz, Plugin.class); + } catch (SecurityException e) { + // If this happens, then ... we're doomed + throw new RuntimeException("Reflection is not allowed.", e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Cannot find register() method in " + clazz, e); + } + } + + // Also bad + throw new IllegalStateException("Spigot could not be found!"); + } + + /** + * Determine if there is a Spigot packet listener. + * @return Spigot packet listener. + */ + public static boolean canUseSpigotListener() { + return getSpigotListenerClass() != null; + } + + // The listener we will register on Spigot. + // Unfortunately, due to the use of PlayerConnection, INetworkManager and Packet, we're + // unable to reference it directly. But with CGLib, it shouldn't cost us much. + private Object dynamicListener; + + // Reference to ProtocolLib + private Plugin plugin; + + // Different sending filters + private IntegerSet queuedFilters; + private IntegerSet reveivedFilters; + + // NetworkManager to injector and player + private ConcurrentMap networkManagerInjector = new ConcurrentHashMap(); + + // Player to injector + private ConcurrentMap playerInjector = new ConcurrentHashMap(); + + // Responsible for informing the PL packet listeners + private ListenerInvoker invoker; + private ErrorReporter reporter; + private Server server; + private ClassLoader classLoader; + + /** + * Create a new spigot injector. + */ + public SpigotPacketInjector(ClassLoader classLoader, ErrorReporter reporter, ListenerInvoker invoker, Server server) { + this.classLoader = classLoader; + this.reporter = reporter; + this.invoker = invoker; + this.server = server; + this.queuedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + this.reveivedFilters = new IntegerSet(Packets.MAXIMUM_PACKET_ID + 1); + } + + public boolean register(Plugin plugin) { + if (hasRegistered()) + return false; + + // Save the plugin too + this.plugin = plugin; + + final Callback[] callbacks = new Callback[3]; + final boolean[] found = new boolean[3]; + + // Packets received from the clients + callbacks[0] = new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return SpigotPacketInjector.this.packetReceived(args[0], args[1], args[2]); + } + }; + // Packet sent/queued + callbacks[1] = new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return SpigotPacketInjector.this.packetQueued(args[0], args[1], args[2]); + } + }; + + // Don't care for everything else + callbacks[2] = NoOp.INSTANCE; + + Enhancer enhancer = new Enhancer(); + enhancer.setClassLoader(classLoader); + enhancer.setSuperclass(getSpigotListenerClass()); + enhancer.setCallbacks(callbacks); + enhancer.setCallbackFilter(new CallbackFilter() { + @Override + public int accept(Method method) { + // We'll be pretty stringent + if (matchMethod("packetReceived", method)) { + found[0] = true; + return 0; + } else if (matchMethod("packetQueued", method)) { + found[1] = true; + return 1; + } else { + found[2] = true; + return 2; + } + } + }); + dynamicListener = enhancer.create(); + + // Verify methods + if (!found[0]) + throw new IllegalStateException("Unable to find a valid packet receiver in Spigot."); + if (!found[1]) + throw new IllegalStateException("Unable to find a valid packet queue in Spigot."); + + // Lets register it too + try { + getRegisterMethod().invoke(null, dynamicListener, plugin); + } catch (Exception e) { + throw new RuntimeException("Cannot register Spigot packet listener.", e); + } + + // If we succeed + return true; + } + + /** + * Determine if the given method is a valid packet receiver or queued method. + * @param methodName - the expected name of the method. + * @param method - the method we're testing. + * @return TRUE if this is a correct method, FALSE otherwise. + */ + private boolean matchMethod(String methodName, Method method) { + return FuzzyMethodContract.newBuilder(). + nameExact(methodName). + parameterCount(3). + parameterSuperOf(MinecraftReflection.getNetHandlerClass(), 1). + parameterSuperOf(MinecraftReflection.getPacketClass(), 2). + returnTypeExact(MinecraftReflection.getPacketClass()). + build(). + isMatch(MethodInfo.fromMethod(method), null); + } + + public boolean hasRegistered() { + return dynamicListener != null; + } + + public PlayerInjectionHandler getPlayerHandler() { + return new DummyPlayerHandler(this, queuedFilters); + } + + public PacketInjector getPacketInjector() { + return new DummyPacketInjector(this, reveivedFilters); + } + + /** + * Retrieve the currently registered injector for the given player. + * @param player - injected player. + * @return The injector. + */ + NetworkObjectInjector getInjector(Player player) { + return playerInjector.get(player); + } + + /** + * Retrieve or create a registered injector for the given network manager and connection. + * @param networkManager - a INetworkManager object. + * @param connection - a Connection (PlayerConnection, PendingConnection) object. + * @return The created NetworkObjectInjector with a temporary player. + */ + NetworkObjectInjector getInjector(Object networkManager, Object connection) { + NetworkObjectInjector dummyInjector = networkManagerInjector.get(networkManager); + + if (dummyInjector == null) { + // Inject the network manager + try { + NetworkObjectInjector created = new NetworkObjectInjector(classLoader, reporter, null, invoker, null); + + if (MinecraftReflection.isLoginHandler(connection)) { + created.initialize(connection); + created.setPlayer(created.createTemporaryPlayer(server)); + } else if (MinecraftReflection.isServerHandler(connection)) { + // Get the player instead + if (playerConnectionPlayer == null) + playerConnectionPlayer = FuzzyReflection.fromObject(connection). + getFieldByType("player", MinecraftReflection.getEntityPlayerClass()); + Object entityPlayer = playerConnectionPlayer.get(connection); + + created.initialize(MinecraftReflection.getBukkitEntity(entityPlayer)); + + } else { + throw new IllegalArgumentException("Unregonized connection in NetworkManager."); + } + + dummyInjector = saveInjector(networkManager, created); + + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot create dummy injector.", e); + } + } + + return dummyInjector; + } + + /** + * Save a given player injector for later. + * @param networkManager - the associated network manager. + * @param created - the created network object creator. + * @return Any other network injector that came before us. + */ + private NetworkObjectInjector saveInjector(Object networkManager, NetworkObjectInjector created) { + // Concurrency - use the same injector! + NetworkObjectInjector result = networkManagerInjector.putIfAbsent(networkManager, created); + + if (result == null) { + result = created; + } + + // Save the player as well + playerInjector.put(created.getPlayer(), created); + return result; + } + + @Override + public Object packetReceived(Object networkManager, Object connection, Object packet) { + Integer id = invoker.getPacketID(packet); + + if (id != null && reveivedFilters.contains(id)) { + // Check for ignored packets + if (ignoredPackets.remove(packet)) { + return packet; + } + + Player sender = getInjector(networkManager, connection).getUpdatedPlayer(); + PacketContainer container = new PacketContainer(id, packet); + PacketEvent event = packetReceived(container, sender); + + if (!event.isCancelled()) + return event.getPacket().getHandle(); + else + return null; // Cancel + } + // Don't change anything + return packet; + } + + @Override + public Object packetQueued(Object networkManager, Object connection, Object packet) { + Integer id = invoker.getPacketID(packet); + + if (id != null & queuedFilters.contains(id)) { + // Check for ignored packets + if (ignoredPackets.remove(packet)) { + return packet; + } + + Player reciever = getInjector(networkManager, connection).getUpdatedPlayer(); + PacketContainer container = new PacketContainer(id, packet); + PacketEvent event = packetQueued(container, reciever); + + if (!event.isCancelled()) + return event.getPacket().getHandle(); + else + return null; // Cancel + } + // Don't change anything + return packet; + } + + /** + * Called to inform the event listeners of a queued packet. + * @param packet - the packet that is to be sent. + * @param reciever - the reciever of this packet. + * @return The packet event that was used. + */ + PacketEvent packetQueued(PacketContainer packet, Player reciever) { + PacketEvent event = PacketEvent.fromServer(this, packet, reciever); + + invoker.invokePacketSending(event); + return event; + } + + /** + * Called to inform the event listeners of a received packet. + * @param packet - the packet that has been receieved. + * @param sender - the client packet. + * @return The packet event that was used. + */ + PacketEvent packetReceived(PacketContainer packet, Player sender) { + PacketEvent event = PacketEvent.fromClient(this, packet, sender); + + invoker.invokePacketRecieving(event); + return event; + } + + /** + * Called when a player has logged in properly. + * @param player - the player that has logged in. + */ + void injectPlayer(Player player) { + try { + NetworkObjectInjector dummy = new NetworkObjectInjector(classLoader, reporter, player, invoker, null); + dummy.initializePlayer(player); + + // Save this player for the network manager + NetworkObjectInjector realInjector = networkManagerInjector.get(dummy.getNetworkManager()); + + if (realInjector != null) { + // Update all future references + realInjector.setUpdatedPlayer(player); + playerInjector.put(player, realInjector); + } else { + // Ah - in that case, save this injector + saveInjector(dummy.getNetworkManager(), dummy); + } + + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot inject " + player); + } + } + + /** + * Uninject the given player. + * @param player - the player to uninject. + */ + void uninjectPlayer(Player player) { + final NetworkObjectInjector injector = getInjector(player); + + if (player != null) { + Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + @Override + public void run() { + // Clean up + playerInjector.remove(injector.getPlayer()); + playerInjector.remove(injector.getUpdatedPlayer()); + networkManagerInjector.remove(injector); + } + }, CLEANUP_DELAY); + } + } + + /** + * Invoked when a plugin wants to sent a packet. + * @param reciever - the packet receiver. + * @param packet - the packet to transmit. + * @param filters - whether or not to invoke the packet listeners. + * @throws InvocationTargetException If anything went wrong. + */ + void sendServerPacket(Player reciever, PacketContainer packet, boolean filters) throws InvocationTargetException { + NetworkObjectInjector networkObject = getInjector(reciever); + + // If TRUE, process this packet like any other + if (filters) + ignoredPackets.remove(packet.getHandle()); + else + ignoredPackets.add(packet.getHandle()); + + if (networkObject != null) + networkObject.sendServerPacket(packet.getHandle(), filters); + else + throw new PlayerLoggedOutException("Player " + reciever + " has logged out"); + } + + /** + * Invoked when a plugin wants to simulate receiving a packet. + * @param player - the supposed sender. + * @param mcPacket - the packet to receieve. + * @throws IllegalAccessException Reflection is not permitted. + * @throws InvocationTargetException Minecraft threw an exception. + */ + void processPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + NetworkObjectInjector networkObject = getInjector(player); + + // We will always ignore this packet + ignoredPackets.add(mcPacket); + + if (networkObject != null) + networkObject.processPacket(mcPacket); + else + throw new PlayerLoggedOutException("Player " + player + " has logged out"); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketListener.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketListener.java new file mode 100644 index 00000000..6a38fb68 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketListener.java @@ -0,0 +1,34 @@ +package com.comphenix.protocol.injector.spigot; + +/** + * Represents a proxy for a Spigot packet listener. + * + * @author Kristian + */ +interface SpigotPacketListener { + /** + * Called when a packet has been received and is about to be handled by the + * current Connection. + *

+ * The returned packet will be the packet passed on for handling, or in the case of + * null being returned, not handled at all. + * + * @param networkManager the NetworkManager receiving the packet + * @param connection the connection which will handle the packet + * @param packet the received packet + * @return the packet to be handled, or null to cancel + */ + public Object packetReceived(Object networkManager, Object connection, Object packet); + + /** + * Called when a packet is queued to be sent.The returned packet will be + * the packet sent. In the case of null being returned, the packet will not + * be sent. + * + * @param networkManager the NetworkManager which will send the packet + * @param connection the connection which queued the packet + * @param packet the queue packet + * @return the packet to be sent, or null if the packet will not be sent. + */ + public Object packetQueued(Object networkManager, Object connection, Object packet); +} 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 4c8cd1bf..9da145aa 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -17,6 +17,7 @@ package com.comphenix.protocol.reflect; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; @@ -26,6 +27,9 @@ import java.util.List; import java.util.Set; import java.util.regex.Pattern; +import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; +import com.google.common.collect.Lists; + /** * Retrieves fields and methods by signature, not just name. * @@ -89,6 +93,42 @@ public class FuzzyReflection { return source; } + /** + * Retrieve the first method that matches. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level method. + * @param matcher - the matcher to use. + * @return The first method that satisfies the given matcher. + * @throws IllegalArgumentException If the method cannot be found. + */ + public Method getMethod(AbstractFuzzyMatcher matcher) { + List result = getMethodList(matcher); + + if (result.size() > 0) + return result.get(0); + else + throw new IllegalArgumentException("Unable to find a method that matches " + matcher); + } + + /** + * Retrieve a list of every method that matches the given matcher. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level methods. + * @param matcher - the matcher to apply. + * @return List of found methods. + */ + public List getMethodList(AbstractFuzzyMatcher matcher) { + List methods = Lists.newArrayList(); + + // Add all matching fields to the list + for (Method method : getMethods()) { + if (matcher.isMatch(MethodInfo.fromMethod(method), source)) { + methods.add(method); + } + } + return methods; + } + /** * Retrieves a method by looking at its name. * @param nameRegex - regular expression that will match method names. @@ -96,7 +136,6 @@ public class FuzzyReflection { * @throws IllegalArgumentException If the method cannot be found. */ public Method getMethodByName(String nameRegex) { - Pattern match = Pattern.compile(nameRegex); for (Method method : getMethods()) { @@ -118,7 +157,6 @@ public class FuzzyReflection { * @throws IllegalArgumentException If the method cannot be found. */ public Method getMethodByParameters(String name, Class... args) { - // Find the correct method to call for (Method method : getMethods()) { if (Arrays.equals(method.getParameterTypes(), args)) { @@ -159,7 +197,6 @@ public class FuzzyReflection { * @throws IllegalArgumentException If the method cannot be found. */ public Method getMethodByParameters(String name, String returnTypeRegex, String[] argsRegex) { - Pattern match = Pattern.compile(returnTypeRegex); Pattern[] argMatch = new Pattern[argsRegex.length]; @@ -199,7 +236,6 @@ public class FuzzyReflection { * @return Every method that satisfies the given constraints. */ public List getMethodListByParameters(Class returnType, Class[] args) { - List methods = new ArrayList(); // Find the correct method to call @@ -219,7 +255,6 @@ public class FuzzyReflection { * @throws IllegalArgumentException If the field cannot be found. */ public Field getFieldByName(String nameRegex) { - Pattern match = Pattern.compile(nameRegex); for (Field field : getFields()) { @@ -241,7 +276,6 @@ public class FuzzyReflection { * @return The first field with a type that is an instance of the given type. */ public Field getFieldByType(String name, Class type) { - List fields = getFieldListByType(type); if (fields.size() > 0) { @@ -260,7 +294,6 @@ public class FuzzyReflection { * @return Every field with a type that is an instance of the given type. */ public List getFieldListByType(Class type) { - List fields = new ArrayList(); // Field with a compatible type @@ -274,6 +307,42 @@ public class FuzzyReflection { return fields; } + /** + * Retrieve the first field that matches. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level fields. + * @param matcher - the matcher to use. + * @return The first method that satisfies the given matcher. + * @throws IllegalArgumentException If the method cannot be found. + */ + public Field getField(AbstractFuzzyMatcher matcher) { + List result = getFieldList(matcher); + + if (result.size() > 0) + return result.get(0); + else + throw new IllegalArgumentException("Unable to find a field that matches " + matcher); + } + + /** + * Retrieve a list of every field that matches the given matcher. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level fields. + * @param matcher - the matcher to apply. + * @return List of found fields. + */ + public List getFieldList(AbstractFuzzyMatcher matcher) { + List fields = Lists.newArrayList(); + + // Add all matching fields to the list + for (Field field : getFields()) { + if (matcher.isMatch(field, source)) { + fields.add(field); + } + } + return fields; + } + /** * Retrieves a field by type. *

@@ -336,8 +405,46 @@ public class FuzzyReflection { typeRegex + " in " + source.getName()); } + /** + * Retrieve the first constructor that matches. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level constructors. + * @param matcher - the matcher to use. + * @return The first constructor that satisfies the given matcher. + * @throws IllegalArgumentException If the constructor cannot be found. + */ + public Constructor getConstructor(AbstractFuzzyMatcher matcher) { + List> result = getConstructorList(matcher); + + if (result.size() > 0) + return result.get(0); + else + throw new IllegalArgumentException("Unable to find a method that matches " + matcher); + } + + /** + * Retrieve a list of every constructor that matches the given matcher. + *

+ * ForceAccess must be TRUE in order for this method to access private, protected and package level constructors. + * @param matcher - the matcher to apply. + * @return List of found constructors. + */ + public List> getConstructorList(AbstractFuzzyMatcher matcher) { + List> constructors = Lists.newArrayList(); + + // Add all matching fields to the list + for (Constructor constructor : getConstructors()) { + if (matcher.isMatch(MethodInfo.fromConstructor(constructor), source)) { + constructors.add(constructor); + } + } + return constructors; + } + /** * Retrieves all private and public fields in declared order (after JDK 1.5). + *

+ * Private, protected and package fields are ignored if forceAccess is FALSE. * @return Every field. */ public Set getFields() { @@ -350,6 +457,8 @@ public class FuzzyReflection { /** * Retrieves all private and public methods in declared order (after JDK 1.5). + *

+ * Private, protected and package methods are ignored if forceAccess is FALSE. * @return Every method. */ public Set getMethods() { @@ -360,6 +469,19 @@ public class FuzzyReflection { return setUnion(source.getMethods()); } + /** + * Retrieves all private and public constructors in declared order (after JDK 1.5). + *

+ * Private, protected and package constructors are ignored if forceAccess is FALSE. + * @return Every constructor. + */ + public Set> getConstructors() { + if (forceAccess) + return setUnion(source.getDeclaredConstructors()); + else + return setUnion(source.getConstructors()); + } + // Prevent duplicate fields private static Set setUnion(T[]... array) { Set result = new LinkedHashSet(); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/MethodInfo.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/MethodInfo.java new file mode 100644 index 00000000..73fdea0b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/MethodInfo.java @@ -0,0 +1,230 @@ +package com.comphenix.protocol.reflect; + +import java.lang.reflect.Constructor; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang.NotImplementedException; + +import com.google.common.collect.Lists; + +/** + * Represents a method or a constructor. + * + * @author Kristian + */ +public abstract class MethodInfo implements GenericDeclaration, Member { + /** + * Wraps a method as a MethodInfo object. + * @param method - the method to wrap. + * @return The wrapped method. + */ + public static MethodInfo fromMethod(final Method method) { + return new MethodInfo() { + @Override + public String getName() { + return method.getName(); + } + @Override + public Class[] getParameterTypes() { + return method.getParameterTypes(); + } + @Override + public Class getDeclaringClass() { + return method.getDeclaringClass(); + } + @Override + public Class getReturnType() { + return method.getReturnType(); + } + @Override + public int getModifiers() { + return method.getModifiers(); + } + @Override + public Class[] getExceptionTypes() { + return method.getExceptionTypes(); + } + @Override + public TypeVariable[] getTypeParameters() { + return method.getTypeParameters(); + } + @Override + public String toGenericString() { + return method.toGenericString(); + } + @Override + public String toString() { + return method.toString(); + } + @Override + public boolean isSynthetic() { + return method.isSynthetic(); + } + @Override + public int hashCode() { + return method.hashCode(); + } + @Override + public boolean isConstructor() { + return false; + } + }; + } + + /** + * Construct a list of method infos from a given array of methods. + * @param methods - array of methods. + * @return Method info list. + */ + public static Collection fromMethods(Method[] methods) { + return fromMethods(Arrays.asList(methods)); + } + + /** + * Construct a list of method infos from a given collection of methods. + * @param methods - list of methods. + * @return Method info list. + */ + public static List fromMethods(Collection methods) { + List infos = Lists.newArrayList(); + + for (Method method : methods) + infos.add(fromMethod(method)); + return infos; + } + + /** + * Wraps a constructor as a method information object. + * @param constructor - the constructor to wrap. + * @return A wrapped constructor. + */ + public static MethodInfo fromConstructor(final Constructor constructor) { + return new MethodInfo() { + @Override + public String getName() { + return constructor.getName(); + } + @Override + public Class[] getParameterTypes() { + return constructor.getParameterTypes(); + } + @Override + public Class getDeclaringClass() { + return constructor.getDeclaringClass(); + } + @Override + public Class getReturnType() { + return Void.class; + } + @Override + public int getModifiers() { + return constructor.getModifiers(); + } + @Override + public Class[] getExceptionTypes() { + return constructor.getExceptionTypes(); + } + @Override + public TypeVariable[] getTypeParameters() { + return constructor.getTypeParameters(); + } + @Override + public String toGenericString() { + return constructor.toGenericString(); + } + @Override + public String toString() { + return constructor.toString(); + } + @Override + public boolean isSynthetic() { + return constructor.isSynthetic(); + } + @Override + public int hashCode() { + return constructor.hashCode(); + } + @Override + public boolean isConstructor() { + return true; + } + }; + } + + /** + * Construct a list of method infos from a given array of constructors. + * @param constructors - array of constructors. + * @return Method info list. + */ + public static Collection fromConstructors(Constructor[] constructors) { + return fromConstructors(Arrays.asList(constructors)); + } + + /** + * Construct a list of method infos from a given collection of constructors. + * @param constructors - list of constructors. + * @return Method info list. + */ + public static List fromConstructors(Collection> constructors) { + List infos = Lists.newArrayList(); + + for (Constructor constructor : constructors) + infos.add(fromConstructor(constructor)); + return infos; + } + + /** + * Returns a string describing this method or constructor + * @return A string representation of the object. + * @see {@link Method#toString()} or {@link Constructor#toString()} + */ + @Override + public String toString() { + throw new NotImplementedException(); + } + + /** + * Returns a string describing this method or constructor, including type parameters. + * @return A string describing this Method, include type parameters + * @see {@link Method#toGenericString()} or {@link Constructor#toGenericString()} + */ + public abstract String toGenericString(); + + /** + * Returns an array of Class objects that represent the types of the exceptions declared to be thrown by the + * underlying method or constructor represented by this MethodInfo object. + * @return The exception types declared as being thrown by the method or constructor this object represents. + * @see {@link Method#getExceptionTypes()} or {@link Constructor#getExceptionTypes()} + */ + public abstract Class[] getExceptionTypes(); + + /** + * Returns a Class object that represents the formal return type of the method or constructor + * represented by this MethodInfo object. + *

+ * This is always {@link Void} for constructors. + * @return The return value, or Void if a constructor. + * @see {@link Method#getReturnType()} + */ + public abstract Class getReturnType(); + + /** + * Returns an array of Class objects that represent the formal parameter types, in declaration order, + * of the method or constructor represented by this MethodInfo object. + * @return The parameter types for the method or constructor this object represents. + * @see {@link Method#getParameterTypes()} or {@link Constructor#getParameterTypes()} + */ + public abstract Class[] getParameterTypes(); + + /** + * Determine if this is a constructor or not. + * @return TRUE if this represents a constructor, FALSE otherwise. + */ + public abstract boolean isConstructor(); +} 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 f33fb1b2..f61c2727 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/PrettyPrinter.java @@ -40,7 +40,6 @@ public class PrettyPrinter { /** * Print the content of an object. * @param object - the object to serialize. - * @param stop - superclass that will stop the process. * @return String representation of the class. * @throws IllegalAccessException */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java index 9d90d0b8..752a14d9 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/StructureModifier.java @@ -26,9 +26,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.comphenix.protocol.reflect.compiler.BackgroundCompiler; +import com.comphenix.protocol.reflect.instances.BannedGenerator; import com.comphenix.protocol.reflect.instances.DefaultInstances; +import com.comphenix.protocol.reflect.instances.InstanceProvider; +import com.comphenix.protocol.utility.MinecraftReflection; import com.google.common.base.Function; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; /** * Provides list-oriented access to the fields of a Minecraft packet. @@ -63,7 +68,19 @@ public class StructureModifier { // Whether or not to automatically compile the structure modifier protected boolean useStructureCompiler; + + // Instance generator we wil use + private static DefaultInstances DEFAULT_GENERATOR = getDefaultGenerator(); + private static DefaultInstances getDefaultGenerator() { + List providers = Lists.newArrayList(); + + // Prevent certain classes from being generated + providers.add(new BannedGenerator(MinecraftReflection.getItemStackClass(), MinecraftReflection.getBlockClass())); + providers.addAll(DefaultInstances.DEFAULT.getRegistered()); + return DefaultInstances.fromCollection(providers); + } + /** * Creates a structure modifier. * @param targetType - the structure to modify. @@ -394,23 +411,13 @@ public class StructureModifier { result = result.withTarget(target); // And the converter, if it's needed - if (!sameConverter(result.converter, converter)) { + if (!Objects.equal(result.converter, converter)) { result = result.withConverter(converter); } return result; } - private boolean sameConverter(EquivalentConverter a, EquivalentConverter b) { - // Compare the converter types - if (a == null) - return b == null; - else if (b == null) - return false; - else - return a.getSpecificType().equals(b.getSpecificType()); - } - /** * Retrieves the common type of each field. * @return Common type of each field. @@ -537,7 +544,7 @@ public class StructureModifier { private static Map generateDefaultFields(List fields) { Map requireDefaults = new HashMap(); - DefaultInstances generator = DefaultInstances.DEFAULT; + DefaultInstances generator = DEFAULT_GENERATOR; int index = 0; for (Field field : fields) { diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java index 62a07bc7..503fb752 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/cloning/FieldCloner.java @@ -20,6 +20,7 @@ package com.comphenix.protocol.reflect.cloning; import com.comphenix.protocol.reflect.ObjectWriter; import com.comphenix.protocol.reflect.StructureModifier; import com.comphenix.protocol.reflect.instances.InstanceProvider; +import com.comphenix.protocol.reflect.instances.NotConstructableException; /** * Represents a class capable of cloning objects by deeply copying its fields. @@ -72,7 +73,11 @@ public class FieldCloner implements Cloner { return false; // Attempt to create the type - return instanceProvider.create(source.getClass()) != null; + try { + return instanceProvider.create(source.getClass()) != null; + } catch (NotConstructableException e) { + return false; + } } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java new file mode 100644 index 00000000..a93ea18f --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMatcher.java @@ -0,0 +1,135 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import com.google.common.primitives.Ints; + +/** + * Represents a matcher for fields, methods, constructors and classes. + *

+ * This class should ideally never expose mutable state. Its round number must be immutable. + * @author Kristian + */ +public abstract class AbstractFuzzyMatcher implements Comparable> { + private Integer roundNumber; + + /** + * Determine if the given value is a match. + * @param value - the value to match. + * @param parent - the parent container, or NULL if this value is the root. + * @return TRUE if it is a match, FALSE otherwise. + */ + public abstract boolean isMatch(T value, Object parent); + + /** + * Calculate the round number indicating when this matcher should be applied. + *

+ * Matchers with a lower round number are applied before matchers with a higher round number. + *

+ * By convention, this round number should be negative, except for zero in the case of a matcher + * that accepts any value. A good implementation should return the inverted tree depth (class hierachy) + * of the least specified type used in the matching. Thus {@link Integer} will have a lower round number than + * {@link Number}. + * + * @return A number (positive or negative) that is used to order matchers. + */ + protected abstract int calculateRoundNumber(); + + /** + * Retrieve the cached round number. This should never change once calculated. + *

+ * Matchers with a lower round number are applied before matchers with a higher round number. + * @return The round number. + * @see {@link #calculateRoundNumber()} + */ + public final int getRoundNumber() { + if (roundNumber == null) { + return roundNumber = calculateRoundNumber(); + } else { + return roundNumber; + } + } + + /** + * Combine two round numbers by taking the highest non-zero number, or return zero. + * @param roundA - the first round number. + * @param roundB - the second round number. + * @return The combined round number. + */ + protected final int combineRounds(int roundA, int roundB) { + if (roundA == 0) + return roundB; + else if (roundB == 0) + return roundA; + else + return Math.max(roundA, roundB); + } + + @Override + public int compareTo(AbstractFuzzyMatcher obj) { + if (obj instanceof AbstractFuzzyMatcher) { + AbstractFuzzyMatcher matcher = (AbstractFuzzyMatcher) obj; + return Ints.compare(getRoundNumber(), matcher.getRoundNumber()); + } + // No match + return -1; + } + + /** + * Create a fuzzy matcher that returns the opposite result of the current matcher. + * @return An inverted fuzzy matcher. + */ + public AbstractFuzzyMatcher inverted() { + return new AbstractFuzzyMatcher() { + @Override + public boolean isMatch(T value, Object parent) { + return !AbstractFuzzyMatcher.this.isMatch(value, parent); + } + + @Override + protected int calculateRoundNumber() { + return -2; + } + }; + } + + /** + * Require that this and the given matcher be TRUE. + * @param other - the other fuzzy matcher. + * @return A combined fuzzy matcher. + */ + public AbstractFuzzyMatcher and(final AbstractFuzzyMatcher other) { + return new AbstractFuzzyMatcher() { + @Override + public boolean isMatch(T value, Object parent) { + // They both have to be true + return AbstractFuzzyMatcher.this.isMatch(value, parent) && + other.isMatch(value, parent); + } + + @Override + protected int calculateRoundNumber() { + return combineRounds(AbstractFuzzyMatcher.this.getRoundNumber(), other.getRoundNumber()); + } + }; + } + + /** + * Require that either this or the other given matcher be TRUE. + * @param other - the other fuzzy matcher. + * @return A combined fuzzy matcher. + */ + public AbstractFuzzyMatcher or(final AbstractFuzzyMatcher other) { + return new AbstractFuzzyMatcher() { + @Override + public boolean isMatch(T value, Object parent) { + // Either can be true + return AbstractFuzzyMatcher.this.isMatch(value, parent) || + other.isMatch(value, parent); + } + + @Override + protected int calculateRoundNumber() { + return combineRounds(AbstractFuzzyMatcher.this.getRoundNumber(), other.getRoundNumber()); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMember.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMember.java new file mode 100644 index 00000000..40147caf --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/AbstractFuzzyMember.java @@ -0,0 +1,295 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.lang.reflect.Member; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +import com.google.common.base.Objects; +import com.google.common.collect.Maps; + +/** + * Represents a matcher that matches members. + * + * @author Kristian + * @param - type that it matches. + */ +public abstract class AbstractFuzzyMember extends AbstractFuzzyMatcher { + // Accessibility matchers + protected int modifiersRequired; + protected int modifiersBanned; + + protected Pattern nameRegex; + protected AbstractFuzzyMatcher> declaringMatcher = ClassExactMatcher.MATCH_ALL; + + /** + * Whether or not this contract can be modified. + */ + protected transient boolean sealed; + + /** + * Represents a builder of a fuzzy member contract. + * + * @author Kristian + */ + public static abstract class Builder> { + protected T member = initialMember(); + + /** + * Add a given bit-field of required modifiers for every matching member. + * @param modifier - bit-field of modifiers that are required. + * @return This builder, for chaining. + */ + public Builder requireModifier(int modifier) { + member.modifiersRequired |= modifier; + return this; + } + + /** + * Add a given bit-field of modifers that will skip or ignore members. + * @param modifier - bit-field of modifiers to skip or ignore. + * @return This builder, for chaining. + */ + public Builder banModifier(int modifier) { + member.modifiersBanned |= modifier; + return this; + } + + /** + * Set the regular expresson that matches a members name. + * @param regex - new regular expression of valid names. + * @return This builder, for chaining. + */ + public Builder nameRegex(String regex) { + member.nameRegex = Pattern.compile(regex); + return this; + } + + /** + * Set the regular expression pattern that matches a members name. + * @param pattern - regular expression pattern for a valid name. + * @return This builder, for chaining. + */ + public Builder nameRegex(Pattern pattern) { + member.nameRegex = pattern; + return this; + } + + /** + * Set the exact name of the member we are matching. + * nameExact(String name) { + return nameRegex(Pattern.quote(name)); + } + + /** + * Require that a member is defined by this exact class. + * @param declaringClass - the declaring class of any matching member. + * @return This builder, for chaining. + */ + public Builder declaringClassExactType(Class declaringClass) { + member.declaringMatcher = FuzzyMatchers.matchExact(declaringClass); + return this; + } + + /** + * Require that a member is defined by this exact class, or any super class. + * @param declaringClass - the declaring class. + * @return This builder, for chaining. + */ + public Builder declaringClassSuperOf(Class declaringClass) { + member.declaringMatcher = FuzzyMatchers.matchSuper(declaringClass); + return this; + } + + /** + * Require that a member is defined by this exact class, or any super class. + * @param declaringClass - the declaring class. + * @return This builder, for chaining. + */ + public Builder declaringClassDerivedOf(Class declaringClass) { + member.declaringMatcher = FuzzyMatchers.matchDerived(declaringClass); + return this; + } + + /** + * Require that a member is defined by a class that matches the given matcher. + * @param classMatcher - class matcher. + * @return This builder, for chaining. + */ + public Builder declaringClassMatching(AbstractFuzzyMatcher> classMatcher) { + member.declaringMatcher = classMatcher; + return this; + } + + /** + * Construct a new instance of the current type. + * @return New instance. + */ + @Nonnull + protected abstract T initialMember(); + + /** + * Build a new instance of this type. + *

+ * Builders should call {@link AbstractFuzzyMember#prepareBuild()} when constructing new objects. + * @return New instance of this type. + */ + public abstract T build(); + } + + protected AbstractFuzzyMember() { + // Only allow construction through the builder + } + + /** + * Called before a builder is building a member and copying its state. + *

+ * Use this to prepare any special values. + */ + protected void prepareBuild() { + // No need to prepare anything + } + + // Clone a given contract + protected AbstractFuzzyMember(AbstractFuzzyMember other) { + this.modifiersRequired = other.modifiersRequired; + this.modifiersBanned = other.modifiersBanned; + this.nameRegex = other.nameRegex; + this.declaringMatcher = other.declaringMatcher; + this.sealed = true; + } + + /** + * Retrieve a bit field of every {@link java.lang.reflect.Modifier Modifier} that is required for the member to match. + * @return A required modifier bit field. + */ + public int getModifiersRequired() { + return modifiersRequired; + } + + /** + * Retrieve a bit field of every {@link java.lang.reflect.Modifier Modifier} that must not be present for the member to match. + * @return A banned modifier bit field. + */ + public int getModifiersBanned() { + return modifiersBanned; + } + + /** + * Retrieve the regular expression pattern that is used to match the name of a member. + * @return The regex matching a name, or NULL if everything matches. + */ + public Pattern getNameRegex() { + return nameRegex; + } + + /** + * Retrieve a class matcher for the declaring class of the member. + * @return An object matching the declaring class. + */ + public AbstractFuzzyMatcher> getDeclaringMatcher() { + return declaringMatcher; + } + + @Override + public boolean isMatch(T value, Object parent) { + int mods = value.getModifiers(); + + // Match accessibility and name + return (mods & modifiersRequired) == modifiersRequired && + (mods & modifiersBanned) == 0 && + declaringMatcher.isMatch(value.getDeclaringClass(), value) && + isNameMatch(value.getName()); + } + + /** + * Determine if a given name matches the current member matcher. + * @param name - the name to match. + * @return TRUE if the name matches, FALSE otherwise. + */ + private boolean isNameMatch(String name) { + if (nameRegex == null) + return true; + else + return nameRegex.matcher(name).matches(); + } + + @Override + protected int calculateRoundNumber() { + // Sanity check + if (!sealed) + throw new IllegalStateException("Cannot calculate round number during construction."); + + // NULL is zero + return declaringMatcher.getRoundNumber(); + } + + @Override + public String toString() { + return getKeyValueView().toString(); + } + + /** + * Generate a view of this matcher as a key-value map. + *

+ * Used by {@link #toString()} to print a representation of this object. + * @return A modifiable key-value view. + */ + protected Map getKeyValueView() { + Map map = Maps.newLinkedHashMap(); + + // Build our representation + if (modifiersRequired != Integer.MAX_VALUE || modifiersBanned != 0) { + map.put("modifiers", String.format("[required: %s, banned: %s]", + getBitView(modifiersRequired, 16), + getBitView(modifiersBanned, 16)) + ); + } + if (nameRegex != null) { + map.put("name", nameRegex.pattern()); + } + if (declaringMatcher != ClassExactMatcher.MATCH_ALL) { + map.put("declaring", declaringMatcher); + } + + return map; + } + + private static String getBitView(int value, int bits) { + if (bits < 0 || bits > 31) + throw new IllegalArgumentException("Bits must be a value between 0 and 32"); + + // Extract our needed bits + int snipped = value & ((1 << bits) - 1); + return Integer.toBinaryString(snipped); + } + + @Override + public boolean equals(Object obj) { + // Immutablity is awesome + if (this == obj) { + return true; + } else if (obj instanceof AbstractFuzzyMember) { + @SuppressWarnings("unchecked") + AbstractFuzzyMember other = (AbstractFuzzyMember) obj; + + return modifiersBanned == other.modifiersBanned && + modifiersRequired == other.modifiersRequired && + FuzzyMatchers.checkPattern(nameRegex, other.nameRegex) && + Objects.equal(declaringMatcher, other.declaringMatcher); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(modifiersBanned, modifiersRequired, + nameRegex != null ? nameRegex.pattern() : null, declaringMatcher); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassExactMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassExactMatcher.java new file mode 100644 index 00000000..58213f70 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassExactMatcher.java @@ -0,0 +1,139 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import com.google.common.base.Objects; + +/** + * Used to check class equality. + * + * @author Kristian + */ +class ClassExactMatcher extends AbstractFuzzyMatcher> { + /** + * Different matching rules. + */ + enum Options { + /** + * Match classes exactly. + */ + MATCH_EXACT, + + /** + * A match if the input class is a superclass of the matcher class, or the same class. + */ + MATCH_SUPER, + + /** + * A match if the input class is a derived class of the matcher class, or the same class. + */ + MATCH_DERIVED + } + + /** + * Match any class. + */ + public static final ClassExactMatcher MATCH_ALL = new ClassExactMatcher(null, Options.MATCH_SUPER); + + private final Class matcher; + private final Options option; + + /** + * Constructs a new class matcher. + * @param matcher - the matching class, or NULL to represent anything. + * @param option - options specifying the matching rules. + */ + ClassExactMatcher(Class matcher, Options option) { + this.matcher = matcher; + this.option = option; + } + + /** + * Determine if a given class is equivalent. + *

+ * If the matcher is NULL, the result will only be TRUE if we're not matching exactly. + * @param input - the input class defined in the source file. + * @param parent - the container that holds a reference to this class. + * @return TRUE if input matches according to the rules in {@link #getOptions()}, FALSE otherwise. + */ + @Override + public boolean isMatch(Class input, Object parent) { + if (input == null) + throw new IllegalArgumentException("Input class cannot be NULL."); + + // Do our checking + if (matcher == null) + return option != Options.MATCH_EXACT; + else if (option == Options.MATCH_SUPER) + return input.isAssignableFrom(matcher); // matcher instanceof input + else if (option == Options.MATCH_DERIVED) + return matcher.isAssignableFrom(input); // input instanceof matcher + else + return input.equals(matcher); + } + + @Override + protected int calculateRoundNumber() { + return -getClassNumber(matcher); + } + + /** + * Retrieve the number of superclasses of the specific class. + *

+ * Object is represented as one. All interfaces are one, unless they're derived. + * @param clazz - the class to test. + * @return The number of superclasses. + */ + public static int getClassNumber(Class clazz) { + int count = 0; + + // Move up the hierachy + while (clazz != null) { + count++; + clazz = clazz.getSuperclass(); + } + return count; + } + + /** + * Retrieve the class we're comparing against. + * @return Class to compare against. + */ + public Class getMatcher() { + return matcher; + } + + /** + * The matching rules for this class matcher. + * @return The current matching option. + */ + public Options getOptions() { + return option; + } + + @Override + public String toString() { + if (option == Options.MATCH_SUPER) + return matcher + " instanceof input"; + else if (option == Options.MATCH_DERIVED) + return "input instanceof " + matcher; + else + return "Exact " + matcher; + } + + @Override + public int hashCode() { + return Objects.hashCode(matcher, option); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ClassExactMatcher) { + ClassExactMatcher other = (ClassExactMatcher) obj; + + return Objects.equal(matcher, other.matcher) && + Objects.equal(option, other.option); + } + return false; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassRegexMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassRegexMatcher.java new file mode 100644 index 00000000..f08d5057 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassRegexMatcher.java @@ -0,0 +1,58 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.util.regex.Pattern; + +import com.google.common.base.Objects; + +/** + * Determine if a class matches based on its name using a regular expression. + * + * @author Kristian + */ +class ClassRegexMatcher extends AbstractFuzzyMatcher> { + private final Pattern regex; + private final int priority; + + public ClassRegexMatcher(Pattern regex, int priority) { + if (regex == null) + throw new IllegalArgumentException("Regular expression pattern cannot be NULL."); + this.regex = regex; + this.priority = priority; + } + + @Override + public boolean isMatch(Class value, Object parent) { + if (value != null) + return regex.matcher(value.getCanonicalName()).matches(); + else + return false; + } + + @Override + protected int calculateRoundNumber() { + return -priority; + } + + @Override + public String toString() { + return "class name of " + regex.toString(); + } + + @Override + public int hashCode() { + return Objects.hashCode(regex, priority); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ClassRegexMatcher) { + ClassRegexMatcher other = (ClassRegexMatcher) obj; + + return priority == other.priority && + FuzzyMatchers.checkPattern(regex, other.regex); + } + return false; + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassSetMatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassSetMatcher.java new file mode 100644 index 00000000..f67a3634 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/ClassSetMatcher.java @@ -0,0 +1,57 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.util.Set; + +import com.google.common.base.Objects; + +/** + * Represents a class matcher that checks for equality using a given set of classes. + * + * @author Kristian + */ +class ClassSetMatcher extends AbstractFuzzyMatcher> { + private final Set> classes; + + public ClassSetMatcher(Set> classes) { + if (classes == null) + throw new IllegalArgumentException("Set of classes cannot be NULL."); + this.classes = classes; + } + + @Override + public boolean isMatch(Class value, Object parent) { + return classes.contains(value); + } + + @Override + protected int calculateRoundNumber() { + int roundNumber = 0; + + // The highest round number (except zero). + for (Class clazz : classes) { + roundNumber = combineRounds(roundNumber, -ClassExactMatcher.getClassNumber(clazz)); + } + return roundNumber; + } + + @Override + public String toString() { + return "match any: " + classes; + } + + @Override + public int hashCode() { + return classes.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ClassSetMatcher) { + // See if the sets are equal + return Objects.equal(classes, ((ClassSetMatcher) obj).classes); + } + return true; + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java new file mode 100644 index 00000000..bb1bfcdb --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyClassContract.java @@ -0,0 +1,240 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.MethodInfo; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +/** + * Determine if a given class implements a given fuzzy (duck typed) contract. + * + * @author Kristian + */ +public class FuzzyClassContract extends AbstractFuzzyMatcher> { + private final ImmutableList> fieldContracts; + private final ImmutableList> methodContracts; + private final ImmutableList> constructorContracts; + + /** + * Represents a class contract builder. + * @author Kristian + * + */ + public static class Builder { + private List> fieldContracts = Lists.newArrayList(); + private List> methodContracts = Lists.newArrayList(); + private List> constructorContracts = Lists.newArrayList(); + + /** + * Add a new field contract. + * @param matcher - new field contract. + * @return This builder, for chaining. + */ + public Builder field(AbstractFuzzyMatcher matcher) { + fieldContracts.add(matcher); + return this; + } + + /** + * Add a new field contract via a builder. + * @param builder - builder for the new field contract. + * @return This builder, for chaining. + */ + public Builder field(FuzzyFieldContract.Builder builder) { + return field(builder.build()); + } + + /** + * Add a new method contract. + * @param matcher - new method contract. + * @return This builder, for chaining. + */ + public Builder method(AbstractFuzzyMatcher matcher) { + methodContracts.add(matcher); + return this; + } + + /** + * Add a new method contract via a builder. + * @param builder - builder for the new method contract. + * @return This builder, for chaining. + */ + public Builder method(FuzzyMethodContract.Builder builder) { + return method(builder.build()); + } + + /** + * Add a new constructor contract. + * @param matcher - new constructor contract. + * @return This builder, for chaining. + */ + public Builder constructor(AbstractFuzzyMatcher matcher) { + constructorContracts.add(matcher); + return this; + } + + /** + * Add a new constructor contract via a builder. + * @param builder - builder for the new constructor contract. + * @return This builder, for chaining. + */ + public Builder constructor(FuzzyMethodContract.Builder builder) { + return constructor(builder.build()); + } + + public FuzzyClassContract build() { + Collections.sort(fieldContracts); + Collections.sort(methodContracts); + Collections.sort(constructorContracts); + + // Construct a new class matcher + return new FuzzyClassContract( + ImmutableList.copyOf(fieldContracts), + ImmutableList.copyOf(methodContracts), + ImmutableList.copyOf(constructorContracts) + ); + } + } + + /** + * Construct a new fuzzy class contract builder. + * @return A new builder. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Constructs a new fuzzy class contract with the given contracts. + * @param fieldContracts - field contracts. + * @param methodContracts - method contracts. + * @param constructorContracts - constructor contracts. + */ + private FuzzyClassContract(ImmutableList> fieldContracts, + ImmutableList> methodContracts, + ImmutableList> constructorContracts) { + super(); + this.fieldContracts = fieldContracts; + this.methodContracts = methodContracts; + this.constructorContracts = constructorContracts; + } + + /** + * Retrieve an immutable list of every field contract. + *

+ * This list is ordered in descending order of priority. + * @return List of every field contract. + */ + public ImmutableList> getFieldContracts() { + return fieldContracts; + } + + /** + * Retrieve an immutable list of every method contract. + *

+ * This list is ordered in descending order of priority. + * @return List of every method contract. + */ + public ImmutableList> getMethodContracts() { + return methodContracts; + } + + /** + * Retrieve an immutable list of every constructor contract. + *

+ * This list is ordered in descending order of priority. + * @return List of every constructor contract. + */ + public ImmutableList> getConstructorContracts() { + return constructorContracts; + } + + @Override + protected int calculateRoundNumber() { + // Find the highest round number + return combineRounds(findHighestRound(fieldContracts), + combineRounds(findHighestRound(methodContracts), + findHighestRound(constructorContracts))); + } + + private int findHighestRound(Collection> list) { + int highest = 0; + + // Go through all the elements + for (AbstractFuzzyMatcher matcher : list) + highest = combineRounds(highest, matcher.getRoundNumber()); + return highest; + } + + @Override + public boolean isMatch(Class value, Object parent) { + FuzzyReflection reflection = FuzzyReflection.fromClass(value, true); + + // Make sure all the contracts are valid + return processContracts(reflection.getFields(), value, fieldContracts) && + processContracts(MethodInfo.fromMethods(reflection.getMethods()), value, methodContracts) && + processContracts(MethodInfo.fromConstructors(value.getDeclaredConstructors()), value, constructorContracts); + } + + private boolean processContracts(Collection values, Class parent, List> matchers) { + boolean[] accepted = new boolean[matchers.size()]; + int count = accepted.length; + + // Process every value in turn + for (T value : values) { + int index = processValue(value, parent, accepted, matchers); + + // See if this worked + if (index >= 0) { + accepted[index] = true; + count--; + } + + // Break early + if (count == 0) + return true; + } + return count == 0; + } + + private int processValue(T value, Class parent, boolean accepted[], List> matchers) { + // The order matters + for (int i = 0; i < matchers.size(); i++) { + if (!accepted[i]) { + AbstractFuzzyMatcher matcher = matchers.get(i); + + // Mark this as detected + if (matcher.isMatch(value, parent)) { + return i; + } + } + } + + // Failure + return -1; + } + + @Override + public String toString() { + Map params = Maps.newLinkedHashMap(); + + if (fieldContracts.size() > 0) { + params.put("fields", fieldContracts); + } + if (methodContracts.size() > 0) { + params.put("methods", methodContracts); + } + if (constructorContracts.size() > 0) { + params.put("constructors", constructorContracts); + } + return "{\n " + Joiner.on(", \n ").join(params.entrySet()) + "\n}"; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java new file mode 100644 index 00000000..4f4929d2 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java @@ -0,0 +1,182 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +import com.google.common.base.Objects; + +/** + * Represents a field matcher. + * + * @author Kristian + */ +public class FuzzyFieldContract extends AbstractFuzzyMember { + private AbstractFuzzyMatcher> typeMatcher = ClassExactMatcher.MATCH_ALL; + + /** + * Represents a builder for a field matcher. + * + * @author Kristian + */ + public static class Builder extends AbstractFuzzyMember.Builder { + @Override + public Builder requireModifier(int modifier) { + super.requireModifier(modifier); + return this; + } + + @Override + public Builder banModifier(int modifier) { + super.banModifier(modifier); + return this; + } + + @Override + public Builder nameRegex(String regex) { + super.nameRegex(regex); + return this; + } + + @Override + public Builder nameRegex(Pattern pattern) { + super.nameRegex(pattern); + return this; + } + + @Override + public Builder nameExact(String name) { + super.nameExact(name); + return this; + } + + public Builder declaringClassExactType(Class declaringClass) { + super.declaringClassExactType(declaringClass); + return this; + } + + @Override + public Builder declaringClassSuperOf(Class declaringClass) { + super.declaringClassSuperOf(declaringClass); + return this; + } + + @Override + public Builder declaringClassDerivedOf(Class declaringClass) { + super.declaringClassDerivedOf(declaringClass); + return this; + } + + @Override + public Builder declaringClassMatching(AbstractFuzzyMatcher> classMatcher) { + super.declaringClassMatching(classMatcher); + return this; + } + + @Override + @Nonnull + protected FuzzyFieldContract initialMember() { + return new FuzzyFieldContract(); + } + + public Builder typeExact(Class type) { + member.typeMatcher = FuzzyMatchers.matchExact(type); + return this; + } + + public Builder typeSuperOf(Class type) { + member.typeMatcher = FuzzyMatchers.matchSuper(type); + return this; + } + + public Builder typeDerivedOf(Class type) { + member.typeMatcher = FuzzyMatchers.matchDerived(type); + return this; + } + + public Builder typeMatches(AbstractFuzzyMatcher> matcher) { + member.typeMatcher = matcher; + return this; + } + + @Override + public FuzzyFieldContract build() { + member.prepareBuild(); + return new FuzzyFieldContract(member); + } + } + + /** + * Return a new fuzzy field contract builder. + * @return New fuzzy field contract builder. + */ + public static Builder newBuilder() { + return new Builder(); + } + + private FuzzyFieldContract() { + // Only allow construction through the builder + super(); + } + + /** + * Retrieve the class matcher that matches the type of a field. + * @return The class matcher. + */ + public AbstractFuzzyMatcher> getTypeMatcher() { + return typeMatcher; + } + + /** + * Create a new field contract from the given contract. + * @param other - the contract to clone. + */ + private FuzzyFieldContract(FuzzyFieldContract other) { + super(other); + this.typeMatcher = other.typeMatcher; + } + + @Override + public boolean isMatch(Field value, Object parent) { + if (super.isMatch(value, parent)) { + return typeMatcher.isMatch(value.getType(), value); + } + // No match + return false; + } + + @Override + protected int calculateRoundNumber() { + // Combine the two + return combineRounds(super.calculateRoundNumber(), + typeMatcher.calculateRoundNumber()); + } + + @Override + protected Map getKeyValueView() { + Map member = super.getKeyValueView(); + + if (typeMatcher != ClassExactMatcher.MATCH_ALL) { + member.put("type", typeMatcher); + } + return member; + } + + @Override + public int hashCode() { + return Objects.hashCode(typeMatcher, super.hashCode()); + } + + @Override + public boolean equals(Object obj) { + // Use the member equals method + if (this == obj) { + return true; + } else if (obj instanceof FuzzyFieldContract && super.equals(obj)) { + return Objects.equal(typeMatcher, ((FuzzyFieldContract) obj).typeMatcher); + } + return true; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMatchers.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMatchers.java new file mode 100644 index 00000000..8e0f5db6 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMatchers.java @@ -0,0 +1,142 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.lang.reflect.Member; +import java.util.Set; +import java.util.regex.Pattern; + +import com.google.common.collect.Sets; + +/** + * Contains factory methods for matching classes. + * + * @author Kristian + */ +public class FuzzyMatchers { + private FuzzyMatchers() { + // Don't make this constructable + } + + /** + * Construct a class matcher that matches types exactly. + * @param matcher - the matching class. + * @return A new class mathcher. + */ + public static AbstractFuzzyMatcher> matchExact(Class matcher) { + return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_EXACT); + } + + /** + * Construct a class matcher that matches any of the given classes exactly. + * @param classes - list of classes to match. + * @return A new class mathcher. + */ + public static AbstractFuzzyMatcher> matchAnyOf(Class... classes) { + return matchAnyOf(Sets.newHashSet(classes)); + } + + /** + * Construct a class matcher that matches any of the given classes exactly. + * @param classes - set of classes to match. + * @return A new class mathcher. + */ + public static AbstractFuzzyMatcher> matchAnyOf(Set> classes) { + return new ClassSetMatcher(classes); + } + + /** + * Construct a class matcher that matches super types of the given class. + * @param matcher - the matching type must be a super class of this type. + * @return A new class mathcher. + */ + public static AbstractFuzzyMatcher> matchSuper(Class matcher) { + return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_SUPER); + } + + /** + * Construct a class matcher that matches derived types of the given class. + * @param matcher - the matching type must be a derived class of this type. + * @return A new class mathcher. + */ + public static AbstractFuzzyMatcher> matchDerived(Class matcher) { + return new ClassExactMatcher(matcher, ClassExactMatcher.Options.MATCH_DERIVED); + } + + /** + * Construct a class matcher based on the canonical names of classes. + * @param regex - regular expression pattern matching class names. + * @param priority - the priority this matcher takes - higher is better. + * @return A fuzzy class matcher based on name. + */ + public static AbstractFuzzyMatcher> matchRegex(final Pattern regex, final int priority) { + return new ClassRegexMatcher(regex, priority); + } + + /** + * Construct a class matcher based on the canonical names of classes. + * @param regex - regular expression matching class names. + * @param priority - the priority this matcher takes - higher is better. + * @return A fuzzy class matcher based on name. + */ + public static AbstractFuzzyMatcher> matchRegex(String regex, final int priority) { + return FuzzyMatchers.matchRegex(Pattern.compile(regex), priority); + } + + /** + * Match the parent class of a method, field or constructor. + * @return Parent matcher. + */ + public static AbstractFuzzyMatcher> matchParent() { + return new AbstractFuzzyMatcher>() { + @Override + public boolean isMatch(Class value, Object parent) { + if (parent instanceof Member) { + return ((Member) parent).getDeclaringClass().equals(value); + } else if (parent instanceof Class) { + return parent.equals(value); + } else { + // Can't be a match + return false; + } + } + + @Override + protected int calculateRoundNumber() { + // We match a very specific type + return -100; + } + + @Override + public String toString() { + return "match parent class"; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(Object obj) { + // If they're the same type, then yes + return obj != null && obj.getClass() == this.getClass(); + } + }; + } + + /** + * Determine if two patterns are the same. + *

+ * Note that two patterns may be functionally the same, but nevertheless be different. + * @param a - the first pattern. + * @param b - the second pattern. + * @return TRUE if they are compiled from the same pattern, FALSE otherwise. + */ + static boolean checkPattern(Pattern a, Pattern b) { + if (a == null) + return b == null; + else if (b == null) + return false; + else + return a.pattern().equals(b.pattern()); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java new file mode 100644 index 00000000..2fe0da85 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java @@ -0,0 +1,539 @@ +package com.comphenix.protocol.reflect.fuzzy; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; + +import org.apache.commons.lang.NotImplementedException; + +import com.comphenix.protocol.reflect.MethodInfo; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +/** + * Represents a contract for matching methods or constructors. + * + * @author Kristian + */ +public class FuzzyMethodContract extends AbstractFuzzyMember { + private static class ParameterClassMatcher extends AbstractFuzzyMatcher[]> { + /** + * The expected index. + */ + private final AbstractFuzzyMatcher> typeMatcher; + private final Integer indexMatch; + + /** + * Construct a new parameter class matcher. + * @param typeMatcher - class type matcher. + */ + public ParameterClassMatcher(@Nonnull AbstractFuzzyMatcher> typeMatcher) { + this(typeMatcher, null); + } + + /** + * Construct a new parameter class matcher. + * @param typeMatcher - class type matcher. + * @param indexMatch - parameter index to match, or NULL for anything. + */ + public ParameterClassMatcher(@Nonnull AbstractFuzzyMatcher> typeMatcher, Integer indexMatch) { + if (typeMatcher == null) + throw new IllegalArgumentException("Type matcher cannot be NULL."); + + this.typeMatcher = typeMatcher; + this.indexMatch = indexMatch; + } + + /** + * See if there's a match for this matcher. + * @param used - parameters that have been matched before. + * @param parent - the container (member) that holds a reference to this parameter. + * @param params - the type of each parameter. + * @return TRUE if this matcher matches any of the given parameters, FALSE otherwise. + */ + public boolean isParameterMatch(Class param, MethodInfo parent, int index) { + // Make sure the index is valid (or NULL) + if (indexMatch == null || indexMatch == index) + return typeMatcher.isMatch(param, parent); + else + return false; + } + + @Override + public boolean isMatch(Class[] value, Object parent) { + throw new NotImplementedException("Use the parameter match instead."); + } + + @Override + protected int calculateRoundNumber() { + return typeMatcher.getRoundNumber(); + } + + @Override + public String toString() { + return String.format("{Type: %s, Index: %s}", typeMatcher, indexMatch); + } + } + + // Match return value + private AbstractFuzzyMatcher> returnMatcher = ClassExactMatcher.MATCH_ALL; + + // Handle parameters and exceptions + private List paramMatchers; + private List exceptionMatchers; + + // Expected parameter count + private Integer paramCount; + + /** + * Represents a builder for a fuzzy method contract. + * + * @author Kristian + */ + public static class Builder extends AbstractFuzzyMember.Builder { + public Builder requireModifier(int modifier) { + super.requireModifier(modifier); + return this; + } + + @Override + public Builder banModifier(int modifier) { + super.banModifier(modifier); + return this; + } + + @Override + public Builder nameRegex(String regex) { + super.nameRegex(regex); + return this; + } + + @Override + public Builder nameRegex(Pattern pattern) { + super.nameRegex(pattern); + return this; + } + + @Override + public Builder nameExact(String name) { + super.nameExact(name); + return this; + } + + @Override + public Builder declaringClassExactType(Class declaringClass) { + super.declaringClassExactType(declaringClass); + return this; + } + + @Override + public Builder declaringClassSuperOf(Class declaringClass) { + super.declaringClassSuperOf(declaringClass); + return this; + } + + @Override + public Builder declaringClassDerivedOf(Class declaringClass) { + super.declaringClassDerivedOf(declaringClass); + return this; + } + + @Override + public Builder declaringClassMatching(AbstractFuzzyMatcher> classMatcher) { + super.declaringClassMatching(classMatcher); + return this; + } + + /** + * Add a new required parameter by type for any matching method. + * @param type - the exact type this parameter must match. + * @return This builder, for chaining. + */ + public Builder parameterExactType(Class type) { + member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type))); + return this; + } + + /** + * Add a new required parameter whose type must be a superclass of the given type. + *

+ * If a parameter is of type Number, any derived class (Integer, Long, etc.) will match it. + * @param type - a type or derived type of the matching parameter. + * @return This builder, for chaining. + */ + public Builder parameterSuperOf(Class type) { + member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type))); + return this; + } + + /** + * Add a new required parameter whose type must match the given class matcher. + * @param classMatcher - the class matcher. + * @return This builder, for chaining. + */ + public Builder parameterMatches(AbstractFuzzyMatcher> classMatcher) { + member.paramMatchers.add(new ParameterClassMatcher(classMatcher)); + return this; + } + + /** + * Add a new required parameter by type and position for any matching method. + * @param type - the exact type this parameter must match. + * @param index - the expected position in the parameter list. + * @return This builder, for chaining. + */ + public Builder parameterExactType(Class type, int index) { + member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type), index)); + return this; + } + + /** + * Add a new required parameter whose type must be a superclass of the given type. + *

+ * If a parameter is of type Number, any derived class (Integer, Long, etc.) will match it. + * @param type - a type or derived type of the matching parameter. + * @param index - the expected position in the parameter list. + * @return This builder, for chaining. + */ + public Builder parameterSuperOf(Class type, int index) { + member.paramMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type), index)); + return this; + } + + /** + * Add a new required parameter whose type must match the given class matcher and index. + * @param classMatcher - the class matcher. + * @param index - the expected position in the parameter list. + * @return This builder, for chaining. + */ + public Builder parameterMatches(AbstractFuzzyMatcher> classMatcher, int index) { + member.paramMatchers.add(new ParameterClassMatcher(classMatcher, index)); + return this; + } + + /** + * Set the expected number of parameters in the matching method. + * @param expectedCount - the number of parameters to expect. + * @return This builder, for chaining. + */ + public Builder parameterCount(int expectedCount) { + member.paramCount = expectedCount; + return this; + } + + /** + * Require a void method. + * @return This builder, for chaining. + */ + public Builder returnTypeVoid() { + return returnTypeExact(Void.TYPE); + } + + /** + * Set the return type of a matching method exactly. + * @param type - the exact return type. + * @return This builder, for chaining. + */ + public Builder returnTypeExact(Class type) { + member.returnMatcher = FuzzyMatchers.matchExact(type); + return this; + } + + /** + * Set the expected super class of the return type for every matching method. + * @param type - the return type, or a super class of it. + * @return This builder, for chaining. + */ + public Builder returnDerivedOf(Class type) { + member.returnMatcher = FuzzyMatchers.matchDerived(type); + return this; + } + + /** + * Set a matcher that must match the return type of a matching method. + * @param classMatcher - the exact return type. + * @return This builder, for chaining. + */ + public Builder returnTypeMatches(AbstractFuzzyMatcher> classMatcher) { + member.returnMatcher = classMatcher; + return this; + } + + /** + * Add a throwable exception that must match the given type exactly. + * @param type - exception type. + * @return This builder, for chaining. + */ + public Builder exceptionExactType(Class type) { + member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type))); + return this; + } + + /** + * Add a throwable exception that must match the given type or be derived. + * @param type - exception type. + * @return This builder, for chaining. + */ + public Builder exceptionSuperOf(Class type) { + member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type))); + return this; + } + + /** + * Add a throwable exception that must match the given matcher, + * @param classMatcher - the class matcher that must match. + * @return This builder, for chaining. + */ + public Builder exceptionMatches(AbstractFuzzyMatcher> classMatcher) { + member.exceptionMatchers.add(new ParameterClassMatcher(classMatcher)); + return this; + } + + /** + * Add a throwable exception that must match the given type exactly and index. + * @param type - exception type. + * @param index - the position in the throwable list. + * @return This builder, for chaining. + */ + public Builder exceptionExactType(Class type, int index) { + member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchExact(type), index)); + return this; + } + + /** + * Add a throwable exception that must match the given type or be derived and index. + * @param type - exception type. + * @param index - the position in the throwable list. + * @return This builder, for chaining. + */ + public Builder exceptionSuperOf(Class type, int index) { + member.exceptionMatchers.add(new ParameterClassMatcher(FuzzyMatchers.matchSuper(type), index)); + return this; + } + + /** + * Add a throwable exception that must match the given matcher and index. + * @param classMatcher - the class matcher that must match. + * @param index - the position in the throwable list. + * @return This builder, for chaining. + */ + public Builder exceptionMatches(AbstractFuzzyMatcher> classMatcher, int index) { + member.exceptionMatchers.add(new ParameterClassMatcher(classMatcher, index)); + return this; + } + + @Override + @Nonnull + protected FuzzyMethodContract initialMember() { + // With mutable lists + return new FuzzyMethodContract(); + } + + @Override + public FuzzyMethodContract build() { + member.prepareBuild(); + return immutableCopy(member); + } + } + + /** + * Return a method contract builder. + * @return Method contract builder. + */ + public static Builder newBuilder() { + return new Builder(); + } + + private FuzzyMethodContract() { + // Only allow construction from the builder + paramMatchers = Lists.newArrayList(); + exceptionMatchers = Lists.newArrayList(); + } + + private FuzzyMethodContract(FuzzyMethodContract other) { + super(other); + this.returnMatcher = other.returnMatcher; + this.paramMatchers = other.paramMatchers; + this.exceptionMatchers = other.exceptionMatchers; + this.paramCount = other.paramCount; + } + + /** + * Construct a new immutable copy of the given method contract. + * @param other - the contract to clone. + * @return A immutable copy of the given contract. + */ + private static FuzzyMethodContract immutableCopy(FuzzyMethodContract other) { + FuzzyMethodContract copy = new FuzzyMethodContract(other); + + // Ensure that the lists are immutable + copy.paramMatchers = ImmutableList.copyOf(copy.paramMatchers); + copy.exceptionMatchers = ImmutableList.copyOf(copy.exceptionMatchers); + return copy; + } + + /** + * Retrieve the class matcher for the return type. + * @return Class matcher for the return type. + */ + public AbstractFuzzyMatcher> getReturnMatcher() { + return returnMatcher; + } + + /** + * Retrieve an immutable list of every parameter matcher for this method. + * @return Immutable list of every parameter matcher. + */ + public ImmutableList getParamMatchers() { + if (paramMatchers instanceof ImmutableList) + return (ImmutableList) paramMatchers; + else + throw new IllegalStateException("Lists haven't been sealed yet."); + } + + /** + * Retrieve an immutable list of every exception matcher for this method. + * @return Immutable list of every exception matcher. + */ + public List getExceptionMatchers() { + if (exceptionMatchers instanceof ImmutableList) + return exceptionMatchers; + else + throw new IllegalStateException("Lists haven't been sealed yet."); + } + + /** + * Retrieve the expected parameter count for this method. + * @return Expected parameter count, or NULL if anyting goes. + */ + public Integer getParamCount() { + return paramCount; + } + + @Override + protected void prepareBuild() { + super.prepareBuild(); + + // Sort lists such that more specific tests are up front + Collections.sort(paramMatchers); + Collections.sort(exceptionMatchers); + } + + @Override + public boolean isMatch(MethodInfo value, Object parent) { + if (super.isMatch(value, parent)) { + Class[] params = value.getParameterTypes(); + Class[] exceptions = value.getExceptionTypes(); + + if (!returnMatcher.isMatch(value.getReturnType(), value)) + return false; + if (paramCount != null && paramCount != value.getParameterTypes().length) + return false; + + // Finally, check parameters and exceptions + return matchParameters(params, value, paramMatchers) && + matchParameters(exceptions, value, exceptionMatchers); + } + // No match + return false; + } + + private boolean matchParameters(Class[] types, MethodInfo parent, List matchers) { + boolean[] accepted = new boolean[matchers.size()]; + int count = accepted.length; + + // Process every parameter in turn + for (int i = 0; i < types.length; i++) { + int matcherIndex = processValue(types[i], parent, i, accepted, matchers); + + if (matcherIndex >= 0) { + accepted[matcherIndex] = true; + count--; + } + + // Break early + if (count == 0) + return true; + } + return count == 0; + } + + private int processValue(Class value, MethodInfo parent, int index, boolean accepted[], List matchers) { + // The order matters + for (int i = 0; i < matchers.size(); i++) { + if (!accepted[i]) { + // See if we got jackpot + if (matchers.get(i).isParameterMatch(value, parent, index)) { + return i; + } + } + } + + // Failure + return -1; + } + + @Override + protected int calculateRoundNumber() { + int current = 0; + + // Consider the return value first + current = returnMatcher.getRoundNumber(); + + // Handle parameters + for (ParameterClassMatcher matcher : paramMatchers) { + current = combineRounds(current, matcher.calculateRoundNumber()); + } + // And exceptions + for (ParameterClassMatcher matcher : exceptionMatchers) { + current = combineRounds(current, matcher.calculateRoundNumber()); + } + + return combineRounds(super.calculateRoundNumber(), current); + } + + @Override + protected Map getKeyValueView() { + Map member = super.getKeyValueView(); + + // Only add fields that are actual contraints + if (returnMatcher != ClassExactMatcher.MATCH_ALL) { + member.put("return", returnMatcher); + } + if (paramMatchers.size() > 0) { + member.put("params", paramMatchers); + } + if (exceptionMatchers.size() > 0) { + member.put("exceptions", exceptionMatchers); + } + if (paramCount != null) { + member.put("paramCount", paramCount); + } + return member; + } + + @Override + public int hashCode() { + return Objects.hashCode(returnMatcher, paramMatchers, exceptionMatchers, paramCount, super.hashCode()); + } + + @Override + public boolean equals(Object obj) { + // Use the member equals method + if (this == obj) { + return true; + } else if (obj instanceof FuzzyMethodContract && super.equals(obj)) { + FuzzyMethodContract other = (FuzzyMethodContract) obj; + + return Objects.equal(paramCount, other.paramCount) && + Objects.equal(returnMatcher, other.returnMatcher) && + Objects.equal(paramMatchers, other.paramMatchers) && + Objects.equal(exceptionMatchers, other.exceptionMatchers); + } + return true; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/BannedGenerator.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/BannedGenerator.java new file mode 100644 index 00000000..b3672c60 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/BannedGenerator.java @@ -0,0 +1,36 @@ +package com.comphenix.protocol.reflect.instances; + +import javax.annotation.Nullable; + +import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; + +/** + * Generator that ensures certain types will never be created. + * + * @author Kristian + */ +public class BannedGenerator implements InstanceProvider { + private AbstractFuzzyMatcher> classMatcher; + + /** + * Construct a generator that ensures any class that matches the given matcher is never constructed. + * @param classMatcher - a class matcher. + */ + public BannedGenerator(AbstractFuzzyMatcher> classMatcher) { + this.classMatcher = classMatcher; + } + + public BannedGenerator(Class... classes) { + this.classMatcher = FuzzyMatchers.matchAnyOf(classes); + } + + @Override + public Object create(@Nullable Class type) { + // Prevent these types from being constructed + if (classMatcher.isMatch(type, null)) { + throw new NotConstructableException(); + } + return null; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java index fe7233da..826a4ecd 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/DefaultInstances.java @@ -86,8 +86,17 @@ public class DefaultInstances implements InstanceProvider { * @param instaceProviders - array of instance providers. * @return An default instance generator. */ - public static DefaultInstances fromArray(InstanceProvider... instaceProviders) { - return new DefaultInstances(ImmutableList.copyOf(instaceProviders)); + public static DefaultInstances fromArray(InstanceProvider... instanceProviders) { + return new DefaultInstances(ImmutableList.copyOf(instanceProviders)); + } + + /** + * Construct a default instance generator using the given instance providers. + * @param instaceProviders - collection of instance providers. + * @return An default instance generator. + */ + public static DefaultInstances fromCollection(Collection instanceProviders) { + return new DefaultInstances(ImmutableList.copyOf(instanceProviders)); } /** @@ -241,11 +250,15 @@ public class DefaultInstances implements InstanceProvider { private T getDefaultInternal(Class type, List providers, int recursionLevel) { // The instance providiers should protect themselves against recursion - for (InstanceProvider generator : providers) { - Object value = generator.create(type); - - if (value != null) - return (T) value; + try { + for (InstanceProvider generator : providers) { + Object value = generator.create(type); + + if (value != null) + return (T) value; + } + } catch (NotConstructableException e) { + return null; } // Guard against recursion @@ -276,7 +289,7 @@ public class DefaultInstances implements InstanceProvider { } } catch (Exception e) { - // Nope, we couldn't create this type + // Nope, we couldn't create this type. Might for instance be NotConstructableException. } // No suitable default value could be found diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/InstanceProvider.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/InstanceProvider.java index 223a2c77..0c71ed92 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/InstanceProvider.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/InstanceProvider.java @@ -25,11 +25,11 @@ import javax.annotation.Nullable; * @author Kristian */ public interface InstanceProvider { - /** * Create an instance given a type, if possible. * @param type - type to create. * @return The instance, or NULL if the type cannot be created. + * @throws NotConstructableException Thrown to indicate that this type cannot or should never be constructed. */ public abstract Object create(@Nullable Class type); } \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/NotConstructableException.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/NotConstructableException.java new file mode 100644 index 00000000..4a7f38e3 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/instances/NotConstructableException.java @@ -0,0 +1,42 @@ +package com.comphenix.protocol.reflect.instances; + +/** + * Invoked when a instance provider indicates that a given type cannot or should not be + * constructed under any circumstances. + * + * @author Kristian + */ +public class NotConstructableException extends IllegalArgumentException { + /** + * Generated by Eclipse. + */ + private static final long serialVersionUID = -1144171604744845463L; + + /** + * Construct a new not constructable exception. + */ + public NotConstructableException() { + super("This object should never be constructed."); + } + + /** + * Construct a new not constructable exception with a custom message. + */ + public NotConstructableException(String message) { + super(message); + } + + /** + * Construct a new not constructable exception with a custom message and cause. + */ + public NotConstructableException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Construct a new not constructable exception with a custom cause. + */ + public NotConstructableException(Throwable cause) { + super( cause); + } +} 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 f9924a3a..3717fe1d 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -17,9 +17,19 @@ package com.comphenix.protocol.utility; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.ServerSocket; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; import javax.annotation.Nonnull; @@ -28,6 +38,13 @@ import org.bukkit.Server; import org.bukkit.inventory.ItemStack; import com.comphenix.protocol.injector.BukkitUnwrapper; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.fuzzy.AbstractFuzzyMatcher; +import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; +import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.google.common.base.Joiner; /** @@ -38,9 +55,17 @@ import com.google.common.base.Joiner; public class MinecraftReflection { /** * Regular expression that matches a Minecraft object. + *

+ * Replaced by the method {@link #getMinecraftObjectRegex()}. */ + @Deprecated public static final String MINECRAFT_OBJECT = "net\\.minecraft(\\.\\w+)+"; + /** + * Regular expression computed dynamically. + */ + private static String DYNAMIC_PACKAGE_MATCHER = null; + /** * The package name of all the classes that belongs to the native code in Minecraft. */ @@ -64,6 +89,28 @@ public class MinecraftReflection { // net.minecraft.server private static Class itemStackArrayClass; + private MinecraftReflection() { + // No need to make this constructable. + } + + /** + * Retrieve a regular expression that can match Minecraft package objects. + * @return Minecraft package matcher. + */ + public static String getMinecraftObjectRegex() { + if (DYNAMIC_PACKAGE_MATCHER == null) + getMinecraftPackage(); + return DYNAMIC_PACKAGE_MATCHER; + } + + /** + * Retrieve a abstract fuzzy class matcher for Minecraft objects. + * @return A matcher for Minecraft objects. + */ + public static AbstractFuzzyMatcher> getMinecraftObjectMatcher() { + return FuzzyMatchers.matchRegex(getMinecraftObjectRegex(), 50); + } + /** * Retrieve the name of the Minecraft server package. * @return Full canonical name of the Minecraft server package. @@ -87,6 +134,25 @@ public class MinecraftReflection { // The return type will tell us the full package, regardless of formating CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName()); MINECRAFT_FULL_PACKAGE = getPackage(returnName); + + // Pretty important invariant + if (!MINECRAFT_FULL_PACKAGE.startsWith(MINECRAFT_PREFIX_PACKAGE)) { + // Assume they're the same instead + MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE; + + // The package is usualy flat, so go with that assumtion + DYNAMIC_PACKAGE_MATCHER = + (MINECRAFT_PREFIX_PACKAGE.length() > 0 ? + Pattern.quote(MINECRAFT_PREFIX_PACKAGE + ".") : "") + "\\w+"; + + // We'll still accept the default location, however + DYNAMIC_PACKAGE_MATCHER = "(" + DYNAMIC_PACKAGE_MATCHER + ")|(" + MINECRAFT_OBJECT + ")"; + + } else { + // Use the standard matcher + DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; + } + return MINECRAFT_FULL_PACKAGE; } catch (SecurityException e) { @@ -108,6 +174,9 @@ public class MinecraftReflection { public static void setMinecraftPackage(String minecraftPackage, String craftBukkitPackage) { MINECRAFT_FULL_PACKAGE = minecraftPackage; CRAFTBUKKIT_PACKAGE = craftBukkitPackage; + + // Standard matcher + DYNAMIC_PACKAGE_MATCHER = MINECRAFT_OBJECT; } /** @@ -126,7 +195,12 @@ public class MinecraftReflection { * @return The package name. */ private static String getPackage(String fullName) { - return fullName.substring(0, fullName.lastIndexOf(".")); + int index = fullName.lastIndexOf("."); + + if (index > 0) + return fullName.substring(0, index); + else + return ""; // Default package } /** @@ -160,6 +234,19 @@ public class MinecraftReflection { return obj.getClass().getName().startsWith(MINECRAFT_PREFIX_PACKAGE); } + /** + * Determine if the given class is found within the package net.minecraft.server, or any equivalent package. + * @param clazz - the class to test. + * @return TRUE if it can, FALSE otherwise. + */ + public static boolean isMinecraftClass(@Nonnull Class clazz) { + if (clazz == null) + throw new IllegalArgumentException("Class cannot be NULL."); + + // Doesn't matter if we don't check for the version here + return clazz.getName().startsWith(MINECRAFT_PREFIX_PACKAGE); + } + /** * Determine if a given object is found in net.minecraft.server, and has the given name. * @param obj - the object to test. @@ -202,7 +289,7 @@ public class MinecraftReflection { } /** - * Determine if the given object is a NetLoginHandler. + * Determine if the given object is a NetLoginHandler (PendingConnection) * @param obj - the given object. * @return TRUE if it is, FALSE otherwise. */ @@ -210,6 +297,15 @@ public class MinecraftReflection { return getNetLoginHandlerClass().isAssignableFrom(obj.getClass()); } + /** + * Determine if the given object is assignable to a NetServerHandler (PlayerConnection) + * @param obj - the given object. + * @return TRUE if it is, FALSE otherwise. + */ + public static boolean isServerHandler(Object obj) { + return getNetServerHandlerClass().isAssignableFrom(obj.getClass()); + } + /** * Determine if the given object is actually a Minecraft packet. * @param obj - the given object. @@ -221,13 +317,22 @@ public class MinecraftReflection { /** * Determine if the given object is a NMS ItemStack. - * @param obj - the given object. + * @param value - the given object. * @return TRUE if it is, FALSE otherwise. */ public static boolean isItemStack(Object value) { return getItemStackClass().isAssignableFrom(value.getClass()); } + /** + * Determine if the given object is a CraftPlayer class. + * @param value - the given object. + * @return TRUE if it is, FALSE otherwise. + */ + public static boolean isCraftPlayer(Object value) { + return getCraftPlayerClass().isAssignableFrom(value.getClass()); + } + /** * Determine if the given object is a Minecraft player entity. * @param obj - the given object. @@ -269,7 +374,22 @@ public class MinecraftReflection { * @return The entity class. */ public static Class getEntityPlayerClass() { - return getMinecraftClass("EntityPlayer"); + try { + return getMinecraftClass("EntityPlayer"); + } catch (RuntimeException e) { + try { + // A fairly stable method + Method detect = FuzzyReflection.fromClass(getCraftBukkitClass("CraftServer")). + getMethodByName("detectListNameConflict"); + + // EntityPlayer is then the first parameter + return detect.getParameterTypes()[0]; + + } catch (IllegalArgumentException ex) { + // Last resort + return fallbackMethodReturn("EntityPlayer", "entity.CraftPlayer", "getHandle"); + } + } } /** @@ -277,7 +397,38 @@ public class MinecraftReflection { * @return The entity class. */ public static Class getEntityClass() { + try { return getMinecraftClass("Entity"); + } catch (RuntimeException e) { + return fallbackMethodReturn("Entity", "entity.CraftEntity", "getHandle"); + } + } + + /** + * Retrieve the WorldServer (NMS) class. + * @return The WorldServer class. + */ + public static Class getWorldServerClass() { + try { + return getMinecraftClass("WorldServer"); + } catch (RuntimeException e) { + return fallbackMethodReturn("WorldServer", "CraftWorld", "getHandle"); + } + } + + /** + * Fallback on the return value of a named method in order to get a NMS class. + * @param nmsClass - the expected name of the Minecraft class. + * @param craftClass - a CraftBukkit class to look at. + * @param methodName - the method we will use. + * @return The return value of this method, which will be saved to the package cache. + */ + private static Class fallbackMethodReturn(String nmsClass, String craftClass, String methodName) { + Class result = FuzzyReflection.fromClass(getCraftBukkitClass(craftClass)). + getMethodByName(methodName).getReturnType(); + + // Save the result + return setMinecraftClass(nmsClass, result); } /** @@ -285,39 +436,160 @@ public class MinecraftReflection { * @return The packet class. */ public static Class getPacketClass() { - return getMinecraftClass("Packet"); + try { + return getMinecraftClass("Packet"); + } catch (RuntimeException e) { + // What kind of class we're looking for (sanity check) + FuzzyClassContract paketContract = + FuzzyClassContract.newBuilder(). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(Map.class). + requireModifier(Modifier.STATIC)). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(Set.class). + requireModifier(Modifier.STATIC)). + method(FuzzyMethodContract.newBuilder(). + parameterSuperOf(DataInputStream.class). + returnTypeVoid()). + build(); + + // Select a method with one Minecraft object parameter + Method selected = FuzzyReflection.fromClass(getNetHandlerClass()). + getMethod(FuzzyMethodContract.newBuilder(). + parameterMatches(paketContract, 0). + parameterCount(1). + build() + ); + + // Save and return + Class clazz = getTopmostClass(selected.getParameterTypes()[0]); + return setMinecraftClass("Packet", clazz); + } } /** - * Retrieve the NetLoginHandler class. + * Retrieve the least derived class, except Object. + * @return Least derived super class. + */ + private static Class getTopmostClass(Class clazz) { + while (true) { + Class superClass = clazz.getSuperclass(); + + if (superClass == Object.class || superClass == null) + return clazz; + else + clazz = superClass; + } + } + + + /** + * Retrieve the MinecraftServer class. + * @return MinecraftServer class. + */ + 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]; + } + } + + /** + * Retrieve the player list class (or ServerConfigurationManager), + * @return The player list class. + */ + public static Class getPlayerListClass() { + try { + return getMinecraftClass("ServerConfigurationManager", "PlayerList"); + } catch (RuntimeException e) { + // Try again + getMinecraftServerClass(); + return getMinecraftClass("ServerConfigurationManager"); + } + } + + /** + * Retrieve the NetLoginHandler class (or PendingConnection) * @return The NetLoginHandler class. */ public static Class getNetLoginHandlerClass() { - return getMinecraftClass("NetLoginHandler", "PendingConnection"); + try { + return getMinecraftClass("NetLoginHandler", "PendingConnection"); + } catch (RuntimeException e) { + Method selected = FuzzyReflection.fromClass(getPlayerListClass()). + getMethod(FuzzyMethodContract.newBuilder(). + parameterMatches( + FuzzyMatchers.matchExact(getEntityPlayerClass()).inverted(), 0 + ). + parameterExactType(String.class, 1). + parameterExactType(String.class, 2). + build() + ); + + // Save the pending connection reference + return setMinecraftClass("NetLoginHandler", selected.getParameterTypes()[0]); + } } /** - * Retrieve the NetServerHandler class. + * Retrieve the NetServerHandler class (or PlayerConnection) * @return The NetServerHandler class. */ public static Class getNetServerHandlerClass() { - return getMinecraftClass("NetServerHandler", "PlayerConnection"); + try { + return getMinecraftClass("NetServerHandler", "PlayerConnection"); + } catch (RuntimeException e) { + // Use the player connection field + return setMinecraftClass("NetLoginHandler", + FuzzyReflection.fromClass(getEntityPlayerClass()). + getFieldByType("playerConnection", getNetHandlerClass()).getType() + ); + } } /** - * Retrieve the NetworkManager class. - * @return The NetworkManager class. + * Retrieve the NetworkManager class or its interface. + * @return The NetworkManager class or its interface. */ public static Class getNetworkManagerClass() { - return getMinecraftClass("NetworkManager"); + try { + return getMinecraftClass("INetworkManager", "NetworkManager"); + } catch (RuntimeException e) { + Constructor selected = FuzzyReflection.fromClass(getNetServerHandlerClass()). + getConstructor(FuzzyMethodContract.newBuilder(). + parameterSuperOf(getMinecraftServerClass(), 0). + parameterSuperOf(getEntityPlayerClass(), 2). + build() + ); + + // And we're done + return setMinecraftClass("INetworkManager", selected.getParameterTypes()[1]); + } } /** - * Retrieve the NetHandler class. + * Retrieve the NetHandler class (or Connection) * @return The NetHandler class. */ public static Class getNetHandlerClass() { - return getMinecraftClass("NetHandler", "Connection"); + try { + return getMinecraftClass("NetHandler", "Connection"); + } catch (RuntimeException e) { + return setMinecraftClass("NetHandler", getNetLoginHandlerClass().getSuperclass()); + } } /** @@ -325,7 +597,43 @@ public class MinecraftReflection { * @return The ItemStack class. */ public static Class getItemStackClass() { - return getMinecraftClass("ItemStack"); + try { + return getMinecraftClass("ItemStack"); + } catch (RuntimeException e) { + // Use the handle reference + return setMinecraftClass("ItemStack", + FuzzyReflection.fromClass(getCraftItemStackClass(), true).getFieldByName("handle").getType()); + } + } + + /** + * Retrieve the Block (NMS) class. + * @return Block (NMS) class. + */ + public static Class getBlockClass() { + try { + return getMinecraftClass("Block"); + } catch (RuntimeException e) { + FuzzyReflection reflect = FuzzyReflection.fromClass(getItemStackClass()); + Set> candidates = new HashSet>(); + + // Minecraft objects in the constructor + for (Constructor constructor : reflect.getConstructors()) { + for (Class clazz : constructor.getParameterTypes()) { + if (isMinecraftClass(clazz)) { + candidates.add(clazz); + } + } + } + + // Useful constructors + Method selected = + reflect.getMethod(FuzzyMethodContract.newBuilder(). + parameterMatches(FuzzyMatchers.matchAnyOf(candidates)). + returnTypeExact(float.class). + build()); + return setMinecraftClass("Block", selected.getParameterTypes()[0]); + } } /** @@ -333,15 +641,21 @@ public class MinecraftReflection { * @return The WorldType class. */ public static Class getWorldTypeClass() { - return getMinecraftClass("WorldType"); - } - - /** - * Retrieve the MinecraftServer class. - * @return MinecraftServer class. - */ - public static Class getMinecraftServerClass() { - return getMinecraftClass("MinecraftServer"); + try { + return getMinecraftClass("WorldType"); + } catch (RuntimeException e) { + // Get the first constructor that matches CraftServer(MINECRAFT_OBJECT, ANY) + Method selected = FuzzyReflection.fromClass(getMinecraftServerClass(), true). + getMethod(FuzzyMethodContract.newBuilder(). + parameterExactType(String.class, 0). + parameterExactType(String.class, 1). + parameterMatches(getMinecraftObjectMatcher()). + parameterExactType(String.class, 4). + parameterCount(5). + build() + ); + return setMinecraftClass("WorldType", selected.getParameterTypes()[3]); + } } /** @@ -349,7 +663,33 @@ public class MinecraftReflection { * @return The DataWatcher class. */ public static Class getDataWatcherClass() { - return getMinecraftClass("DataWatcher"); + try { + return getMinecraftClass("DataWatcher"); + } catch (RuntimeException e) { + // Describe the DataWatcher + FuzzyClassContract dataWatcherContract = FuzzyClassContract.newBuilder(). + field(FuzzyFieldContract.newBuilder(). + requireModifier(Modifier.STATIC). + typeDerivedOf(Map.class)). + field(FuzzyFieldContract.newBuilder(). + banModifier(Modifier.STATIC). + typeDerivedOf(Map.class)). + method(FuzzyMethodContract.newBuilder(). + parameterExactType(int.class). + parameterExactType(Object.class). + returnTypeVoid()). + build(); + FuzzyFieldContract fieldContract = FuzzyFieldContract.newBuilder(). + typeMatches(dataWatcherContract). + build(); + + // Get such a field and save the result + return setMinecraftClass("DataWatcher", + FuzzyReflection.fromClass(getEntityClass(), true). + getField(fieldContract). + getType() + ); + } } /** @@ -357,7 +697,25 @@ public class MinecraftReflection { * @return The ChunkPosition class. */ public static Class getChunkPositionClass() { - return getMinecraftClass("ChunkPosition"); + try { + return getMinecraftClass("ChunkPosition"); + } catch (RuntimeException e) { + Class normalChunkGenerator = getCraftBukkitClass("generator.NormalChunkGenerator"); + + // ChunkPosition a(net.minecraft.server.World world, String string, int i, int i1, int i2) { + FuzzyMethodContract selected = FuzzyMethodContract.newBuilder(). + banModifier(Modifier.STATIC). + parameterMatches(getMinecraftObjectMatcher(), 0). + parameterExactType(String.class, 1). + parameterExactType(int.class, 2). + parameterExactType(int.class, 3). + parameterExactType(int.class, 4). + build(); + + return setMinecraftClass("ChunkPosition", + FuzzyReflection.fromClass(normalChunkGenerator). + getMethod(selected).getReturnType()); + } } /** @@ -365,7 +723,11 @@ public class MinecraftReflection { * @return The ChunkPosition class. */ public static Class getChunkCoordinatesClass() { - return getMinecraftClass("ChunkCoordinates"); + try { + return getMinecraftClass("ChunkCoordinates"); + } catch (RuntimeException e) { + return setMinecraftClass("ChunkCoordinates", WrappedDataWatcher.getTypeClass(6)); + } } /** @@ -373,7 +735,46 @@ public class MinecraftReflection { * @return The WatchableObject class. */ public static Class getWatchableObjectClass() { - return getMinecraftClass("WatchableObject"); + try { + return getMinecraftClass("WatchableObject"); + } catch (RuntimeException e) { + Method selected = FuzzyReflection.fromClass(getDataWatcherClass(), true). + getMethod(FuzzyMethodContract.newBuilder(). + requireModifier(Modifier.STATIC). + parameterSuperOf(DataOutputStream.class, 0). + parameterMatches(getMinecraftObjectMatcher(), 1). + build()); + + // Use the second parameter + return setMinecraftClass("WatchableObject", selected.getParameterTypes()[1]); + } + } + + /** + * Retrieve the ServerConnection abstract class. + * @return The ServerConnection class. + */ + public static Class getServerConnectionClass() { + try { + return getMinecraftClass("ServerConnection"); + } catch (RuntimeException e) { + FuzzyClassContract serverConnectionContract = FuzzyClassContract.newBuilder(). + constructor(FuzzyMethodContract.newBuilder(). + parameterExactType(getMinecraftServerClass()). + parameterCount(1)). + method(FuzzyMethodContract.newBuilder(). + parameterExactType(getNetServerHandlerClass())). + build(); + + Method selected = FuzzyReflection.fromClass(getMinecraftServerClass()). + getMethod(FuzzyMethodContract.newBuilder(). + requireModifier(Modifier.ABSTRACT). + returnTypeMatches(serverConnectionContract). + build()); + + // Use the return type + return setMinecraftClass("ServerConnection", selected.getReturnType()); + } } /** @@ -381,7 +782,95 @@ public class MinecraftReflection { * @return The NBT base class. */ public static Class getNBTBaseClass() { - return getMinecraftClass("NBTBase"); + try { + return getMinecraftClass("NBTBase"); + } catch (RuntimeException e) { + FuzzyClassContract tagCompoundContract = FuzzyClassContract.newBuilder(). + constructor(FuzzyMethodContract.newBuilder(). + parameterExactType(String.class). + parameterCount(1)). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(Map.class)). + build(); + + Method selected = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()). + getMethod(FuzzyMethodContract.newBuilder(). + requireModifier(Modifier.STATIC). + parameterSuperOf(DataInputStream.class). + parameterCount(1). + returnTypeMatches(tagCompoundContract). + build() + ); + + // Use the return type here too + return setMinecraftClass("NBTBase", selected.getReturnType()); + } + } + + /** + * Retrieve the EntityTracker (NMS) class. + * @return EntityTracker class. + */ + public static Class getEntityTrackerClass() { + try { + return getMinecraftClass("EntityTracker"); + } catch (RuntimeException e) { + FuzzyClassContract entityTrackerContract = FuzzyClassContract.newBuilder(). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(Set.class)). + method(FuzzyMethodContract.newBuilder(). + parameterSuperOf(MinecraftReflection.getEntityClass()). + parameterCount(1). + returnTypeVoid()). + method(FuzzyMethodContract.newBuilder(). + parameterSuperOf(MinecraftReflection.getEntityClass(), 0). + parameterSuperOf(int.class, 1). + parameterSuperOf(int.class, 2). + parameterCount(3). + returnTypeVoid()). + build(); + + Field selected = FuzzyReflection.fromClass(MinecraftReflection.getWorldServerClass(), true). + getField(FuzzyFieldContract.newBuilder(). + typeMatches(entityTrackerContract). + build() + ); + + // Go by the defined type of this field + return setMinecraftClass("EntityTracker", selected.getType()); + } + } + + /** + * Retrieve the NetworkListenThread class (NMS). + *

+ * Note that this class was removed after Minecraft 1.3.1. + * @return NetworkListenThread class. + */ + public static Class getNetworkListenThreadClass() { + try { + return getMinecraftClass("NetworkListenThread"); + } catch (RuntimeException e) { + FuzzyClassContract networkListenContract = FuzzyClassContract.newBuilder(). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(ServerSocket.class)). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(Thread.class)). + field(FuzzyFieldContract.newBuilder(). + typeDerivedOf(List.class)). + method(FuzzyMethodContract.newBuilder(). + parameterExactType(getNetServerHandlerClass())). + build(); + + Field selected = FuzzyReflection.fromClass(MinecraftReflection.getMinecraftServerClass(), true). + getField(FuzzyFieldContract.newBuilder(). + typeMatches(networkListenContract). + build() + ); + + // Go by the defined type of this field + return setMinecraftClass("NetworkListenThread", selected.getType()); + } } /** @@ -411,6 +900,14 @@ public class MinecraftReflection { public static Class getCraftItemStackClass() { return getCraftBukkitClass("inventory.CraftItemStack"); } + + /** + * Retrieve the CraftPlayer class. + * @return CraftPlayer class. + */ + public static Class getCraftPlayerClass() { + return getCraftBukkitClass("entity.CraftPlayer"); + } /** * Retrieve a CraftItemStack from a given ItemStack. @@ -547,9 +1044,23 @@ public class MinecraftReflection { return minecraftPackage.getPackageClass(className); } + /** + * Set the class object for the specific Minecraft class. + * @param className - name of the Minecraft class. + * @param clazz - the new class object. + * @return The provided clazz object. + */ + private static Class setMinecraftClass(String className, Class clazz) { + if (minecraftPackage == null) + minecraftPackage = new CachedPackage(getMinecraftPackage()); + minecraftPackage.setPackageClass(className, clazz); + return clazz; + } + /** * Retrieve the first class that matches a specified Minecraft name. - * @param classes - the specific Minecraft class. + * @param className - the specific Minecraft class. + * @param aliases - alternative names for this Minecraft class. * @return Class object. * @throws RuntimeException If we are unable to find any of the given classes. */ @@ -600,4 +1111,6 @@ public class MinecraftReflection { public static String getNetLoginHandlerName() { return getNetLoginHandlerClass().getSimpleName(); } + + } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java index ac4fe360..2a441149 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java @@ -6,6 +6,9 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.lang.reflect.Method; + +import javax.annotation.Nonnull; + import org.bukkit.inventory.ItemStack; import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; @@ -28,10 +31,12 @@ public class StreamSerializer { * and {@link java.io.DataInputStream DataInputStream}. * * @param input - the target input stream. - * @return The resulting item stack. + * @return The resulting item stack, or NULL if the serialized item stack was NULL. * @throws IOException If the operation failed due to reflection or corrupt data. */ - public ItemStack deserializeItemStack(DataInputStream input) throws IOException { + public ItemStack deserializeItemStack(@Nonnull DataInputStream input) throws IOException { + if (input == null) + throw new IllegalArgumentException("Input stream cannot be NULL."); if (readItemMethod == null) readItemMethod = FuzzyReflection.fromClass(MinecraftReflection.getPacketClass()). getMethodByParameters("readPacket", @@ -51,10 +56,13 @@ public class StreamSerializer { /** * Deserialize an item stack from a base-64 encoded string. * @param input - base-64 encoded string. - * @return A deserialized item stack. + * @return A deserialized item stack, or NULL if the serialized ItemStack was also NULL. * @throws IOException If the operation failed due to reflection or corrupt data. */ - public ItemStack deserializeItemStack(String input) throws IOException { + public ItemStack deserializeItemStack(@Nonnull String input) throws IOException { + if (input == null) + throw new IllegalArgumentException("Input text cannot be NULL."); + ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(input)); return deserializeItemStack(new DataInputStream(inputStream)); @@ -65,12 +73,18 @@ public class StreamSerializer { *

* To supply a byte array, wrap it in a {@link java.io.ByteArrayOutputStream ByteArrayOutputStream} * and {@link java.io.DataOutputStream DataOutputStream}. + *

+ * Note: An ItemStack can be written to a stream even if it's NULL. * * @param output - the target output stream. - * @param stack - the item stack that will be written. + * @param stack - the item stack that will be written, or NULL to represent air/nothing. * @throws IOException If the operation fails due to reflection problems. */ - public void serializeItemStack(DataOutputStream output, ItemStack stack) throws IOException { + public void serializeItemStack(@Nonnull DataOutputStream output, ItemStack stack) throws IOException { + if (output == null) + throw new IllegalArgumentException("Output stream cannot be NULL."); + + // Get the NMS version of the ItemStack Object nmsItem = MinecraftReflection.getMinecraftItemStack(stack); if (writeItemMethod == null) @@ -87,7 +101,10 @@ public class StreamSerializer { /** * Serialize an item stack as a base-64 encoded string. - * @param stack - the item stack to serialize. + *

+ * Note: An ItemStack can be written to the serialized text even if it's NULL. + * + * @param stack - the item stack to serialize, or NULL to represent air/nothing. * @return A base-64 representation of the given item stack. * @throws IOException If the operation fails due to reflection problems. */ diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index 95606336..5d3be1fa 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -36,6 +36,7 @@ import com.comphenix.protocol.reflect.instances.DefaultInstances; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.wrappers.nbt.NbtBase; import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.google.common.base.Objects; /** * Contains several useful equivalent converters for normal Bukkit types. @@ -58,12 +59,104 @@ public class BukkitConverters { } } + /** + * Represents a typical equivalence converter. + * + * @author Kristian + * @param - type that can be converted. + */ + private static abstract class IgnoreNullConverter implements EquivalentConverter { + public final Object getGeneric(Class genericType, TType specific) { + if (specific != null) + return getGenericValue(genericType, specific); + else + return null; + } + + /** + * Retrieve a copy of the actual generic value. + * @param genericType - generic type. + * @param specific - the specific type- + * @return A copy of the specific type. + */ + protected abstract Object getGenericValue(Class genericType, TType specific); + + @Override + public final TType getSpecific(Object generic) { + if (generic != null) + return getSpecificValue(generic); + else + return null; + } + + /** + * Retrieve a copy of the specific type using an instance of the generic type. + * @param generic - generic type. + * @return A copy of the specific type. + */ + protected abstract TType getSpecificValue(Object generic); + + @Override + public boolean equals(Object obj) { + // Very short + if (this == obj) + return true; + if (obj == null) + return false; + + // See if they're equivalent + if (obj instanceof EquivalentConverter) { + @SuppressWarnings("rawtypes") + EquivalentConverter other = (EquivalentConverter) obj; + return Objects.equal(this.getSpecificType(), other.getSpecificType()); + } + return false; + } + } + + /** + * Represents a converter that is only valid in a given world. + * + * @author Kristian + * @param - instance types it converts. + */ + private static abstract class WorldSpecificConverter extends IgnoreNullConverter { + protected World world; + + /** + * Initialize a new world-specificn converter. + * @param world - the given world. + */ + public WorldSpecificConverter(World world) { + super(); + this.world = world; + } + + @Override + public boolean equals(Object obj) { + // More shortcuts + if (obj == this) + return true; + if (obj == null) + return false; + + // Add another constraint + if (obj instanceof WorldSpecificConverter && super.equals(obj)) { + @SuppressWarnings("rawtypes") + WorldSpecificConverter other = (WorldSpecificConverter) obj; + + return Objects.equal(world, other.world); + } + return false; + } + } + public static EquivalentConverter> getListConverter(final Class genericItemType, final EquivalentConverter itemConverter) { // Convert to and from the wrapper - return getIgnoreNull(new EquivalentConverter>() { + return new IgnoreNullConverter>() { @SuppressWarnings("unchecked") @Override - public List getSpecific(Object generic) { + protected List getSpecificValue(Object generic) { if (generic instanceof Collection) { List items = new ArrayList(); @@ -83,7 +176,7 @@ public class BukkitConverters { @SuppressWarnings("unchecked") @Override - public Object getGeneric(Class genericType, List specific) { + protected Object getGenericValue(Class genericType, List specific) { Collection newContainer = (Collection) DefaultInstances.DEFAULT.getDefault(genericType); // Convert each object @@ -105,8 +198,7 @@ public class BukkitConverters { Class dummy = List.class; return (Class>) dummy; } - } - ); + }; } /** @@ -114,13 +206,13 @@ public class BukkitConverters { * @return A watchable object converter. */ public static EquivalentConverter getWatchableObjectConverter() { - return getIgnoreNull(new EquivalentConverter() { + return new IgnoreNullConverter() { @Override - public Object getGeneric(Class genericType, WrappedWatchableObject specific) { + protected Object getGenericValue(Class genericType, WrappedWatchableObject specific) { return specific.getHandle(); } - public WrappedWatchableObject getSpecific(Object generic) { + protected WrappedWatchableObject getSpecificValue(Object generic) { if (MinecraftReflection.isWatchableObject(generic)) return new WrappedWatchableObject(generic); else if (generic instanceof WrappedWatchableObject) @@ -133,7 +225,7 @@ public class BukkitConverters { public Class getSpecificType() { return WrappedWatchableObject.class; } - }); + }; } /** @@ -141,14 +233,14 @@ public class BukkitConverters { * @return A DataWatcher converter. */ public static EquivalentConverter getDataWatcherConverter() { - return getIgnoreNull(new EquivalentConverter() { + return new IgnoreNullConverter() { @Override - public Object getGeneric(Class genericType, WrappedDataWatcher specific) { + protected Object getGenericValue(Class genericType, WrappedDataWatcher specific) { return specific.getHandle(); } @Override - public WrappedDataWatcher getSpecific(Object generic) { + protected WrappedDataWatcher getSpecificValue(Object generic) { if (MinecraftReflection.isDataWatcher(generic)) return new WrappedDataWatcher(generic); else if (generic instanceof WrappedDataWatcher) @@ -161,7 +253,7 @@ public class BukkitConverters { public Class getSpecificType() { return WrappedDataWatcher.class; } - }); + }; } /** @@ -173,9 +265,9 @@ public class BukkitConverters { if (!hasWorldType) return null; - return getIgnoreNull(new EquivalentConverter() { + return new IgnoreNullConverter() { @Override - public Object getGeneric(Class genericType, WorldType specific) { + protected Object getGenericValue(Class genericType, WorldType specific) { try { if (worldTypeGetType == null) worldTypeGetType = MinecraftReflection.getWorldTypeClass().getMethod("getType", String.class); @@ -189,7 +281,7 @@ public class BukkitConverters { } @Override - public WorldType getSpecific(Object generic) { + protected WorldType getSpecificValue(Object generic) { try { if (worldTypeName == null) worldTypeName = MinecraftReflection.getWorldTypeClass().getMethod("name"); @@ -207,7 +299,7 @@ public class BukkitConverters { public Class getSpecificType() { return WorldType.class; } - }); + }; } /** @@ -215,14 +307,14 @@ public class BukkitConverters { * @return An equivalent converter for NBT. */ public static EquivalentConverter> getNbtConverter() { - return getIgnoreNull(new EquivalentConverter>() { + return new IgnoreNullConverter>() { @Override - public Object getGeneric(Class genericType, NbtBase specific) { + protected Object getGenericValue(Class genericType, NbtBase specific) { return NbtFactory.fromBase(specific).getHandle(); } @Override - public NbtBase getSpecific(Object generic) { + protected NbtBase getSpecificValue(Object generic) { return NbtFactory.fromNMS(generic); } @@ -233,7 +325,7 @@ public class BukkitConverters { Class dummy = NbtBase.class; return (Class>) dummy; } - }); + }; } /** @@ -242,27 +334,25 @@ public class BukkitConverters { * @return A converter between the underlying NMS entity and Bukkit's wrapper. */ public static EquivalentConverter getEntityConverter(World world) { - final World container = world; final WeakReference managerRef = new WeakReference(ProtocolLibrary.getProtocolManager()); - return getIgnoreNull(new EquivalentConverter() { - + return new WorldSpecificConverter(world) { @Override - public Object getGeneric(Class genericType, Entity specific) { + public Object getGenericValue(Class genericType, Entity specific) { // Simple enough return specific.getEntityId(); } @Override - public Entity getSpecific(Object generic) { + public Entity getSpecificValue(Object generic) { try { Integer id = (Integer) generic; ProtocolManager manager = managerRef.get(); - // Use the + // Use the entity ID to get a reference to the entity if (id != null && manager != null) { - return manager.getEntityFromID(container, id); + return manager.getEntityFromID(world, id); } else { return null; } @@ -276,7 +366,7 @@ public class BukkitConverters { public Class getSpecificType() { return Entity.class; } - }); + }; } /** @@ -284,13 +374,14 @@ public class BukkitConverters { * @return Item stack converter. */ public static EquivalentConverter getItemStackConverter() { - return getIgnoreNull(new EquivalentConverter() { - public Object getGeneric(Class genericType, ItemStack specific) { + return new IgnoreNullConverter() { + @Override + protected Object getGenericValue(Class genericType, ItemStack specific) { return MinecraftReflection.getMinecraftItemStack(specific); } @Override - public ItemStack getSpecific(Object generic) { + protected ItemStack getSpecificValue(Object generic) { return MinecraftReflection.getBukkitItemStack(generic); } @@ -298,7 +389,7 @@ public class BukkitConverters { public Class getSpecificType() { return ItemStack.class; } - }); + }; } /** @@ -308,20 +399,15 @@ public class BukkitConverters { */ public static EquivalentConverter getIgnoreNull(final EquivalentConverter delegate) { // Automatically wrap all parameters to the delegate with a NULL check - return new EquivalentConverter() { - public Object getGeneric(Class genericType, TType specific) { - if (specific != null) - return delegate.getGeneric(genericType, specific); - else - return null; + return new IgnoreNullConverter() { + @Override + public Object getGenericValue(Class genericType, TType specific) { + return delegate.getGeneric(genericType, specific); } @Override - public TType getSpecific(Object generic) { - if (generic != null) - return delegate.getSpecific(generic); - else - return null; + public TType getSpecificValue(Object generic) { + return delegate.getSpecific(generic); } @Override diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java index 012d9458..fda3ea21 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedDataWatcher.java @@ -410,7 +410,7 @@ public class WrappedDataWatcher implements Iterable { try { Object watchable = getWatchedObject(index); - + if (watchable != null) { new WrappedWatchableObject(watchable).setValue(newValue, update); } else { @@ -551,8 +551,17 @@ public class WrappedDataWatcher implements Iterable { List candidates = fuzzy.getMethodListByParameters(Void.TYPE, new Class[] { int.class, Object.class}); - for (Method method : candidates) { + // Load the get-method + try { + getKeyValueMethod = fuzzy.getMethodByParameters( + "getWatchableObject", MinecraftReflection.getWatchableObjectClass(), new Class[] { int.class }); + getKeyValueMethod.setAccessible(true); + } catch (IllegalArgumentException e) { + // Use the fallback method + } + + for (Method method : candidates) { if (!method.getName().startsWith("watch")) { createKeyValueMethod = method; } else { @@ -569,15 +578,21 @@ public class WrappedDataWatcher implements Iterable { } else { throw new IllegalStateException("Unable to find create and update watchable object. Update ProtocolLib."); } - } - - // Load the get-method - try { - getKeyValueMethod = fuzzy.getMethodByParameters( - "getWatchableObject", ".*WatchableObject", new String[] { int.class.getName() }); - getKeyValueMethod.setAccessible(true); - } catch (IllegalArgumentException e) { - // Use fallback method + + // Be a little scientist - see if this in fact IS the right way around + try { + WrappedDataWatcher watcher = new WrappedDataWatcher(); + watcher.setObject(0, 0); + watcher.setObject(0, 1); + + if (watcher.getInteger(0) != 1) { + throw new IllegalStateException("This cannot be!"); + } + } catch (Exception e) { + // Nope + updateKeyValueMethod = candidates.get(0); + createKeyValueMethod = candidates.get(1); + } } } diff --git a/ProtocolLib/src/main/resources/plugin.yml b/ProtocolLib/src/main/resources/plugin.yml index 174c6b4a..50beb220 100644 --- a/ProtocolLib/src/main/resources/plugin.yml +++ b/ProtocolLib/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: ProtocolLib -version: 2.1.0 +version: 2.2.0 description: Provides read/write access to the Minecraft protocol. author: Comphenix website: http://www.comphenix.net/ProtocolLib