Merge branch 'master' into gh-pages
Dieser Commit ist enthalten in:
Commit
fc44ff9fba
@ -2,7 +2,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>2.1.0</version>
|
||||
<version>2.2.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<description>Provides read/write access to the Minecraft protocol.</description>
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 <a href="http://www.wiki.vg/Protocol_History#2012-12-20">Protocol History - MinecraftCoalition</a>
|
||||
*/
|
||||
@Deprecated()
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 AbstractConcurrentListenerMultimap<AsyncList
|
||||
public PacketProcessingQueue(PlayerSendingHandler sendingHandler, int initialSize, int maximumSize, int maximumConcurrency) {
|
||||
super();
|
||||
|
||||
try {
|
||||
this.processingQueue = Synchronization.queue(MinMaxPriorityQueue.
|
||||
expectedSize(initialSize).
|
||||
maximumSize(maximumSize).
|
||||
<PacketEventHolder>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<PacketEventHolder>(), null);
|
||||
}
|
||||
|
||||
this.maximumConcurrency = maximumConcurrency;
|
||||
this.concurrentProcessing = new Semaphore(maximumConcurrency);
|
||||
|
@ -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) {
|
@ -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)
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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<Integer> 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<Integer> getClientPackets() throws FieldAccessException {
|
||||
return MinecraftRegistry.getClientPackets();
|
||||
return PacketRegistry.getClientPackets();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<Object> 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<Object> value = new StructureModifier<Object>(
|
||||
MinecraftRegistry.getPacketClassFromID(id, true), MinecraftReflection.getPacketClass(), true);
|
||||
PacketRegistry.getPacketClassFromID(id, true), MinecraftReflection.getPacketClass(), true);
|
||||
|
||||
result = structureModifiers.putIfAbsent(id, value);
|
||||
|
||||
|
@ -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<Integer> 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();
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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<Class, Integer> 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);
|
||||
}
|
||||
}
|
@ -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<Integer, Class> overwritten = MinecraftRegistry.getOverwrittenPackets();
|
||||
Map<Integer, Class> previous = MinecraftRegistry.getPreviousPackets();
|
||||
Map<Class, Integer> registry = MinecraftRegistry.getPacketToID();
|
||||
Class old = MinecraftRegistry.getPacketClassFromID(packetID);
|
||||
Map<Integer, Class> overwritten = PacketRegistry.getOverwrittenPackets();
|
||||
Map<Integer, Class> previous = PacketRegistry.getPreviousPackets();
|
||||
Map<Class, Integer> 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<Class, Integer> registry = MinecraftRegistry.getPacketToID();
|
||||
Map<Integer, Class> previous = MinecraftRegistry.getPreviousPackets();
|
||||
Map<Integer, Class> overwritten = MinecraftRegistry.getOverwrittenPackets();
|
||||
Map<Class, Integer> registry = PacketRegistry.getPacketToID();
|
||||
Map<Integer, Class> previous = PacketRegistry.getPreviousPackets();
|
||||
Map<Integer, Class> 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<Integer> 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<Integer, Class> overwritten = MinecraftRegistry.getOverwrittenPackets();
|
||||
Map<Integer, Class> previous = MinecraftRegistry.getPreviousPackets();
|
||||
Map<Integer, Class> overwritten = PacketRegistry.getOverwrittenPackets();
|
||||
Map<Integer, Class> previous = PacketRegistry.getPreviousPackets();
|
||||
|
||||
// Remove every packet handler
|
||||
for (Integer id : previous.keySet().toArray(new Integer[0])) {
|
@ -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<Object, Object> override = Collections.synchronizedMap(new WeakHashMap<Object, Object>());
|
||||
|
||||
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;
|
@ -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<Object> {
|
||||
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<Object> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
getFieldByType("networkListenThread", MinecraftReflection.getNetworkListenThreadClass());
|
||||
} catch (RuntimeException e) {
|
||||
reporter.reportDetailed(this, "Cannot find listener thread in MinecraftServer.", e, minecraftServer);
|
||||
return;
|
||||
|
@ -38,7 +38,7 @@ class NetLoginInjector {
|
||||
private ConcurrentMap<Object, PlayerInjector> 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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
if (sendPacketMethod == null) {
|
||||
try {
|
||||
sendPacketMethod = FuzzyReflection.fromObject(serverHandler).getMethodByName("sendPacket.*");
|
||||
} catch (IllegalArgumentException e) {
|
||||
Map<String, Method> netServer = getMethodList(
|
||||
MinecraftReflection.getNetServerHandlerClass(), MinecraftReflection.getPacketClass());
|
||||
Map<String, Method> 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<String, Method> getMethodList(Class<?> source, Class<?>... params) {
|
||||
return getMappedMethods(
|
||||
FuzzyReflection.fromClass(source, true).
|
||||
getMethodListByParameters(Void.TYPE, params)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve every method as a map over names.
|
||||
* <p>
|
||||
* 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<String, Method> getMappedMethods(List<Method> methods) {
|
||||
Map<String, Method> 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;
|
||||
@ -297,7 +350,21 @@ 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);
|
||||
// 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.");
|
||||
}
|
||||
|
@ -1,199 +1,56 @@
|
||||
/*
|
||||
* 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<SocketAddress, PlayerInjector> addressLookup = Maps.newConcurrentMap();
|
||||
private Map<Player, PlayerInjector> playerInjection = Maps.newConcurrentMap();
|
||||
|
||||
// Lookup player by connection
|
||||
private BlockingHashMap<DataInputStream, PlayerInjector> 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<PacketListener> packetListeners;
|
||||
|
||||
// The class loader we're using
|
||||
private ClassLoader classLoader;
|
||||
|
||||
// Used to filter injection attempts
|
||||
private Predicate<GamePhase> injectionFilter;
|
||||
|
||||
public PlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate<GamePhase> injectionFilter,
|
||||
ListenerInvoker invoker, Set<PacketListener> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.");
|
||||
}
|
||||
}
|
||||
public abstract void removePacketHandler(int packetID);
|
||||
|
||||
/**
|
||||
* Retrieve a player by its DataInput connection.
|
||||
@ -201,9 +58,8 @@ public class PlayerInjectionHandler {
|
||||
* @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.
|
||||
@ -213,26 +69,7 @@ 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.
|
||||
@ -240,215 +77,20 @@ public class PlayerInjectionHandler {
|
||||
* 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
public abstract void injectPlayer(Player player);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
public abstract void handleDisconnect(Player player);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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;
|
||||
}
|
||||
public abstract boolean uninjectPlayer(Player player);
|
||||
|
||||
/**
|
||||
* Unregisters a player by the given address.
|
||||
@ -459,18 +101,7 @@ 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.
|
||||
@ -479,18 +110,8 @@ 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.
|
||||
@ -499,61 +120,14 @@ 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<PacketListener> listeners) {
|
||||
// Make sure the current listeners are compatible
|
||||
if (lastSuccessfulHook != null) {
|
||||
for (PacketListener listener : listeners) {
|
||||
checkListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract void checkListener(Set<PacketListener> listeners);
|
||||
|
||||
/**
|
||||
* Determine if a listener is valid or not.
|
||||
@ -561,71 +135,22 @@ public class PlayerInjectionHandler {
|
||||
* 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
public abstract void checkListener(PacketListener listener);
|
||||
|
||||
/**
|
||||
* Retrieve the current list of registered sending listeners.
|
||||
* @return List of the sending listeners's packet IDs.
|
||||
*/
|
||||
public Set<Integer> getSendingFilters() {
|
||||
return sendingFilters.toSet();
|
||||
}
|
||||
public abstract Set<Integer> getSendingFilters();
|
||||
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
@ -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) {
|
||||
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,10 +572,12 @@ 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.");
|
||||
if (inputField == null)
|
||||
inputField = FuzzyReflection.fromObject(networkManager, true).
|
||||
getFieldByType("java\\.io\\.DataInputStream");
|
||||
|
||||
// Get the associated input stream
|
||||
try {
|
||||
@ -598,6 +608,16 @@ abstract class PlayerInjector {
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hooked player.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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<GamePhase> injectionFilter;
|
||||
protected ListenerInvoker invoker;
|
||||
protected Set<PacketListener> 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<GamePhase> 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<PacketListener> 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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -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<SocketAddress, PlayerInjector> addressLookup = Maps.newConcurrentMap();
|
||||
private Map<Player, PlayerInjector> playerInjection = Maps.newConcurrentMap();
|
||||
|
||||
// Lookup player by connection
|
||||
private BlockingHashMap<DataInputStream, PlayerInjector> 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<PacketListener> packetListeners;
|
||||
|
||||
// The class loader we're using
|
||||
private ClassLoader classLoader;
|
||||
|
||||
// Used to filter injection attempts
|
||||
private Predicate<GamePhase> injectionFilter;
|
||||
|
||||
public ProxyPlayerInjectionHandler(ClassLoader classLoader, ErrorReporter reporter, Predicate<GamePhase> injectionFilter,
|
||||
ListenerInvoker invoker, Set<PacketListener> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<PacketListener> 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.
|
||||
* <p>
|
||||
* 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<Integer> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -81,7 +81,7 @@ class TemporaryPlayerFactory {
|
||||
* </ul>
|
||||
* <p>
|
||||
* 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.
|
||||
|
@ -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<Integer> getPacketHandlers() {
|
||||
return reveivedFilters.toSet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketEvent packetRecieved(PacketContainer packet, Player client) {
|
||||
return injector.packetReceived(packet, client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupAll() {
|
||||
reveivedFilters.clear();
|
||||
}
|
||||
}
|
@ -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<Integer> 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<PacketListener> listeners) {
|
||||
// Yes, really
|
||||
}
|
||||
}
|
@ -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<Object> ignoredPackets = Collections.newSetFromMap(new MapMaker().weakKeys().<Object, Boolean>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<Object, NetworkObjectInjector> networkManagerInjector = new ConcurrentHashMap<Object, NetworkObjectInjector>();
|
||||
|
||||
// Player to injector
|
||||
private ConcurrentMap<Player, NetworkObjectInjector> playerInjector = new ConcurrentHashMap<Player, NetworkObjectInjector>();
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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<MethodInfo> matcher) {
|
||||
List<Method> 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.
|
||||
* <p>
|
||||
* 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<Method> getMethodList(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
List<Method> 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<Method> getMethodListByParameters(Class<?> returnType, Class<?>[] args) {
|
||||
|
||||
List<Method> methods = new ArrayList<Method>();
|
||||
|
||||
// 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<Field> 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<Field> getFieldListByType(Class<?> type) {
|
||||
|
||||
List<Field> fields = new ArrayList<Field>();
|
||||
|
||||
// Field with a compatible type
|
||||
@ -274,6 +307,42 @@ public class FuzzyReflection {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first field that matches.
|
||||
* <p>
|
||||
* 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<Field> matcher) {
|
||||
List<Field> 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.
|
||||
* <p>
|
||||
* 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<Field> getFieldList(AbstractFuzzyMatcher<Field> matcher) {
|
||||
List<Field> 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.
|
||||
* <p>
|
||||
@ -336,8 +405,46 @@ public class FuzzyReflection {
|
||||
typeRegex + " in " + source.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the first constructor that matches.
|
||||
* <p>
|
||||
* 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<MethodInfo> matcher) {
|
||||
List<Constructor<?>> 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.
|
||||
* <p>
|
||||
* 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<Constructor<?>> getConstructorList(AbstractFuzzyMatcher<MethodInfo> matcher) {
|
||||
List<Constructor<?>> 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).
|
||||
* <p>
|
||||
* Private, protected and package fields are ignored if forceAccess is FALSE.
|
||||
* @return Every field.
|
||||
*/
|
||||
public Set<Field> getFields() {
|
||||
@ -350,6 +457,8 @@ public class FuzzyReflection {
|
||||
|
||||
/**
|
||||
* Retrieves all private and public methods in declared order (after JDK 1.5).
|
||||
* <p>
|
||||
* Private, protected and package methods are ignored if forceAccess is FALSE.
|
||||
* @return Every method.
|
||||
*/
|
||||
public Set<Method> 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).
|
||||
* <p>
|
||||
* Private, protected and package constructors are ignored if forceAccess is FALSE.
|
||||
* @return Every constructor.
|
||||
*/
|
||||
public Set<Constructor<?>> getConstructors() {
|
||||
if (forceAccess)
|
||||
return setUnion(source.getDeclaredConstructors());
|
||||
else
|
||||
return setUnion(source.getConstructors());
|
||||
}
|
||||
|
||||
// Prevent duplicate fields
|
||||
private static <T> Set<T> setUnion(T[]... array) {
|
||||
Set<T> result = new LinkedHashSet<T>();
|
||||
|
230
ProtocolLib/src/main/java/com/comphenix/protocol/reflect/MethodInfo.java
Normale Datei
230
ProtocolLib/src/main/java/com/comphenix/protocol/reflect/MethodInfo.java
Normale Datei
@ -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<MethodInfo> 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<MethodInfo> fromMethods(Collection<Method> methods) {
|
||||
List<MethodInfo> 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<MethodInfo> 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<MethodInfo> fromConstructors(Collection<Constructor<?>> constructors) {
|
||||
List<MethodInfo> 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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.
|
||||
@ -64,6 +69,18 @@ public class StructureModifier<TField> {
|
||||
// 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<InstanceProvider> 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<TField> {
|
||||
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<TField> {
|
||||
private static Map<Field, Integer> generateDefaultFields(List<Field> fields) {
|
||||
|
||||
Map<Field, Integer> requireDefaults = new HashMap<Field, Integer>();
|
||||
DefaultInstances generator = DefaultInstances.DEFAULT;
|
||||
DefaultInstances generator = DEFAULT_GENERATOR;
|
||||
int index = 0;
|
||||
|
||||
for (Field field : fields) {
|
||||
|
@ -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
|
||||
try {
|
||||
return instanceProvider.create(source.getClass()) != null;
|
||||
} catch (NotConstructableException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* This class should ideally never expose mutable state. Its round number must be immutable.
|
||||
* @author Kristian
|
||||
*/
|
||||
public abstract class AbstractFuzzyMatcher<T> implements Comparable<AbstractFuzzyMatcher<T>> {
|
||||
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.
|
||||
* <p>
|
||||
* Matchers with a lower round number are applied before matchers with a higher round number.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<T> 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<T> inverted() {
|
||||
return new AbstractFuzzyMatcher<T>() {
|
||||
@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<T> and(final AbstractFuzzyMatcher<T> other) {
|
||||
return new AbstractFuzzyMatcher<T>() {
|
||||
@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<T> or(final AbstractFuzzyMatcher<T> other) {
|
||||
return new AbstractFuzzyMatcher<T>() {
|
||||
@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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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 <T> - type that it matches.
|
||||
*/
|
||||
public abstract class AbstractFuzzyMember<T extends Member> extends AbstractFuzzyMatcher<T> {
|
||||
// Accessibility matchers
|
||||
protected int modifiersRequired;
|
||||
protected int modifiersBanned;
|
||||
|
||||
protected Pattern nameRegex;
|
||||
protected AbstractFuzzyMatcher<Class<?>> 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<T extends AbstractFuzzyMember<?>> {
|
||||
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<T> 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<T> 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<T> 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<T> nameRegex(Pattern pattern) {
|
||||
member.nameRegex = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the exact name of the member we are matching.
|
||||
* <p<
|
||||
* This will overwrite the regular expression rule.
|
||||
* @param name - exact name.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder<T> 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<T> 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<T> 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<T> 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<T> declaringClassMatching(AbstractFuzzyMatcher<Class<?>> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Use this to prepare any special values.
|
||||
*/
|
||||
protected void prepareBuild() {
|
||||
// No need to prepare anything
|
||||
}
|
||||
|
||||
// Clone a given contract
|
||||
protected AbstractFuzzyMember(AbstractFuzzyMember<T> 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<Class<?>> 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.
|
||||
* <p>
|
||||
* Used by {@link #toString()} to print a representation of this object.
|
||||
* @return A modifiable key-value view.
|
||||
*/
|
||||
protected Map<String, Object> getKeyValueView() {
|
||||
Map<String, Object> 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<T> other = (AbstractFuzzyMember<T>) 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);
|
||||
}
|
||||
}
|
@ -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<Class<?>> {
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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<Class<?>> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<Class<?>> {
|
||||
private final Set<Class<?>> classes;
|
||||
|
||||
public ClassSetMatcher(Set<Class<?>> 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;
|
||||
}
|
||||
}
|
@ -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<Class<?>> {
|
||||
private final ImmutableList<AbstractFuzzyMatcher<Field>> fieldContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> methodContracts;
|
||||
private final ImmutableList<AbstractFuzzyMatcher<MethodInfo>> constructorContracts;
|
||||
|
||||
/**
|
||||
* Represents a class contract builder.
|
||||
* @author Kristian
|
||||
*
|
||||
*/
|
||||
public static class Builder {
|
||||
private List<AbstractFuzzyMatcher<Field>> fieldContracts = Lists.newArrayList();
|
||||
private List<AbstractFuzzyMatcher<MethodInfo>> methodContracts = Lists.newArrayList();
|
||||
private List<AbstractFuzzyMatcher<MethodInfo>> constructorContracts = Lists.newArrayList();
|
||||
|
||||
/**
|
||||
* Add a new field contract.
|
||||
* @param matcher - new field contract.
|
||||
* @return This builder, for chaining.
|
||||
*/
|
||||
public Builder field(AbstractFuzzyMatcher<Field> 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<MethodInfo> 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<MethodInfo> 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<AbstractFuzzyMatcher<Field>> fieldContracts,
|
||||
ImmutableList<AbstractFuzzyMatcher<MethodInfo>> methodContracts,
|
||||
ImmutableList<AbstractFuzzyMatcher<MethodInfo>> constructorContracts) {
|
||||
super();
|
||||
this.fieldContracts = fieldContracts;
|
||||
this.methodContracts = methodContracts;
|
||||
this.constructorContracts = constructorContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every field contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
* @return List of every field contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<Field>> getFieldContracts() {
|
||||
return fieldContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every method contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
* @return List of every method contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<MethodInfo>> getMethodContracts() {
|
||||
return methodContracts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every constructor contract.
|
||||
* <p>
|
||||
* This list is ordered in descending order of priority.
|
||||
* @return List of every constructor contract.
|
||||
*/
|
||||
public ImmutableList<AbstractFuzzyMatcher<MethodInfo>> getConstructorContracts() {
|
||||
return constructorContracts;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateRoundNumber() {
|
||||
// Find the highest round number
|
||||
return combineRounds(findHighestRound(fieldContracts),
|
||||
combineRounds(findHighestRound(methodContracts),
|
||||
findHighestRound(constructorContracts)));
|
||||
}
|
||||
|
||||
private <T> int findHighestRound(Collection<AbstractFuzzyMatcher<T>> list) {
|
||||
int highest = 0;
|
||||
|
||||
// Go through all the elements
|
||||
for (AbstractFuzzyMatcher<T> 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 <T> boolean processContracts(Collection<T> values, Class<?> parent, List<AbstractFuzzyMatcher<T>> 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 <T> int processValue(T value, Class<?> parent, boolean accepted[], List<AbstractFuzzyMatcher<T>> matchers) {
|
||||
// The order matters
|
||||
for (int i = 0; i < matchers.size(); i++) {
|
||||
if (!accepted[i]) {
|
||||
AbstractFuzzyMatcher<T> matcher = matchers.get(i);
|
||||
|
||||
// Mark this as detected
|
||||
if (matcher.isMatch(value, parent)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Failure
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Map<String, Object> 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}";
|
||||
}
|
||||
}
|
@ -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<Field> {
|
||||
private AbstractFuzzyMatcher<Class<?>> typeMatcher = ClassExactMatcher.MATCH_ALL;
|
||||
|
||||
/**
|
||||
* Represents a builder for a field matcher.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static class Builder extends AbstractFuzzyMember.Builder<FuzzyFieldContract> {
|
||||
@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<Class<?>> 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<Class<?>> 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<Class<?>> 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<String, Object> getKeyValueView() {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
@ -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<Class<?>> 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<Class<?>> 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<Class<?>> matchAnyOf(Set<Class<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> matchParent() {
|
||||
return new AbstractFuzzyMatcher<Class<?>>() {
|
||||
@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.
|
||||
* <p>
|
||||
* 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());
|
||||
}
|
||||
}
|
@ -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<MethodInfo> {
|
||||
private static class ParameterClassMatcher extends AbstractFuzzyMatcher<Class<?>[]> {
|
||||
/**
|
||||
* The expected index.
|
||||
*/
|
||||
private final AbstractFuzzyMatcher<Class<?>> typeMatcher;
|
||||
private final Integer indexMatch;
|
||||
|
||||
/**
|
||||
* Construct a new parameter class matcher.
|
||||
* @param typeMatcher - class type matcher.
|
||||
*/
|
||||
public ParameterClassMatcher(@Nonnull AbstractFuzzyMatcher<Class<?>> 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<Class<?>> 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<Class<?>> returnMatcher = ClassExactMatcher.MATCH_ALL;
|
||||
|
||||
// Handle parameters and exceptions
|
||||
private List<ParameterClassMatcher> paramMatchers;
|
||||
private List<ParameterClassMatcher> exceptionMatchers;
|
||||
|
||||
// Expected parameter count
|
||||
private Integer paramCount;
|
||||
|
||||
/**
|
||||
* Represents a builder for a fuzzy method contract.
|
||||
*
|
||||
* @author Kristian
|
||||
*/
|
||||
public static class Builder extends AbstractFuzzyMember.Builder<FuzzyMethodContract> {
|
||||
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<Class<?>> 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.
|
||||
* <p>
|
||||
* 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<Class<?>> 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.
|
||||
* <p>
|
||||
* 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<Class<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> getReturnMatcher() {
|
||||
return returnMatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an immutable list of every parameter matcher for this method.
|
||||
* @return Immutable list of every parameter matcher.
|
||||
*/
|
||||
public ImmutableList<ParameterClassMatcher> getParamMatchers() {
|
||||
if (paramMatchers instanceof ImmutableList)
|
||||
return (ImmutableList<ParameterClassMatcher>) 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<ParameterClassMatcher> 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<ParameterClassMatcher> 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<ParameterClassMatcher> 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<String, Object> getKeyValueView() {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
@ -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<Class<?>> classMatcher;
|
||||
|
||||
/**
|
||||
* Construct a generator that ensures any class that matches the given matcher is never constructed.
|
||||
* @param classMatcher - a class matcher.
|
||||
*/
|
||||
public BannedGenerator(AbstractFuzzyMatcher<Class<?>> 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;
|
||||
}
|
||||
}
|
@ -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<InstanceProvider> instanceProviders) {
|
||||
return new DefaultInstances(ImmutableList.copyOf(instanceProviders));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,12 +250,16 @@ public class DefaultInstances implements InstanceProvider {
|
||||
private <T> T getDefaultInternal(Class<T> type, List<InstanceProvider> providers, int recursionLevel) {
|
||||
|
||||
// The instance providiers should protect themselves against recursion
|
||||
try {
|
||||
for (InstanceProvider generator : providers) {
|
||||
Object value = generator.create(type);
|
||||
|
||||
if (value != null)
|
||||
return (T) value;
|
||||
}
|
||||
} catch (NotConstructableException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Guard against recursion
|
||||
if (recursionLevel >= maximumRecursion) {
|
||||
@ -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
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* 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<Class<?>> 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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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<Class<?>> candidates = new HashSet<Class<?>>();
|
||||
|
||||
// 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() {
|
||||
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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the MinecraftServer class.
|
||||
* @return MinecraftServer class.
|
||||
*/
|
||||
public static Class<?> getMinecraftServerClass() {
|
||||
return getMinecraftClass("MinecraftServer");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -349,7 +663,33 @@ public class MinecraftReflection {
|
||||
* @return The DataWatcher class.
|
||||
*/
|
||||
public static Class<?> getDataWatcherClass() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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).
|
||||
* <p>
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -412,6 +901,14 @@ public class MinecraftReflection {
|
||||
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.
|
||||
* @param bukkitItemStack - the Bukkit ItemStack to convert.
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
* <p>
|
||||
* To supply a byte array, wrap it in a {@link java.io.ByteArrayOutputStream ByteArrayOutputStream}
|
||||
* and {@link java.io.DataOutputStream DataOutputStream}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
|
@ -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 <T> - type that can be converted.
|
||||
*/
|
||||
private static abstract class IgnoreNullConverter<TType> implements EquivalentConverter<TType> {
|
||||
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 <TType> - instance types it converts.
|
||||
*/
|
||||
private static abstract class WorldSpecificConverter<TType> extends IgnoreNullConverter<TType> {
|
||||
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 <T> EquivalentConverter<List<T>> getListConverter(final Class<?> genericItemType, final EquivalentConverter<T> itemConverter) {
|
||||
// Convert to and from the wrapper
|
||||
return getIgnoreNull(new EquivalentConverter<List<T>>() {
|
||||
return new IgnoreNullConverter<List<T>>() {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public List<T> getSpecific(Object generic) {
|
||||
protected List<T> getSpecificValue(Object generic) {
|
||||
if (generic instanceof Collection) {
|
||||
List<T> items = new ArrayList<T>();
|
||||
|
||||
@ -83,7 +176,7 @@ public class BukkitConverters {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object getGeneric(Class<?> genericType, List<T> specific) {
|
||||
protected Object getGenericValue(Class<?> genericType, List<T> specific) {
|
||||
Collection<Object> newContainer = (Collection<Object>) DefaultInstances.DEFAULT.getDefault(genericType);
|
||||
|
||||
// Convert each object
|
||||
@ -105,8 +198,7 @@ public class BukkitConverters {
|
||||
Class<?> dummy = List.class;
|
||||
return (Class<List<T>>) dummy;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,13 +206,13 @@ public class BukkitConverters {
|
||||
* @return A watchable object converter.
|
||||
*/
|
||||
public static EquivalentConverter<WrappedWatchableObject> getWatchableObjectConverter() {
|
||||
return getIgnoreNull(new EquivalentConverter<WrappedWatchableObject>() {
|
||||
return new IgnoreNullConverter<WrappedWatchableObject>() {
|
||||
@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<WrappedWatchableObject> getSpecificType() {
|
||||
return WrappedWatchableObject.class;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,14 +233,14 @@ public class BukkitConverters {
|
||||
* @return A DataWatcher converter.
|
||||
*/
|
||||
public static EquivalentConverter<WrappedDataWatcher> getDataWatcherConverter() {
|
||||
return getIgnoreNull(new EquivalentConverter<WrappedDataWatcher>() {
|
||||
return new IgnoreNullConverter<WrappedDataWatcher>() {
|
||||
@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<WrappedDataWatcher> getSpecificType() {
|
||||
return WrappedDataWatcher.class;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,9 +265,9 @@ public class BukkitConverters {
|
||||
if (!hasWorldType)
|
||||
return null;
|
||||
|
||||
return getIgnoreNull(new EquivalentConverter<WorldType>() {
|
||||
return new IgnoreNullConverter<WorldType>() {
|
||||
@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<WorldType> getSpecificType() {
|
||||
return WorldType.class;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,14 +307,14 @@ public class BukkitConverters {
|
||||
* @return An equivalent converter for NBT.
|
||||
*/
|
||||
public static EquivalentConverter<NbtBase<?>> getNbtConverter() {
|
||||
return getIgnoreNull(new EquivalentConverter<NbtBase<?>>() {
|
||||
return new IgnoreNullConverter<NbtBase<?>>() {
|
||||
@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<NbtBase<?>>) dummy;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,27 +334,25 @@ public class BukkitConverters {
|
||||
* @return A converter between the underlying NMS entity and Bukkit's wrapper.
|
||||
*/
|
||||
public static EquivalentConverter<Entity> getEntityConverter(World world) {
|
||||
final World container = world;
|
||||
final WeakReference<ProtocolManager> managerRef =
|
||||
new WeakReference<ProtocolManager>(ProtocolLibrary.getProtocolManager());
|
||||
|
||||
return getIgnoreNull(new EquivalentConverter<Entity>() {
|
||||
|
||||
return new WorldSpecificConverter<Entity>(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<Entity> getSpecificType() {
|
||||
return Entity.class;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,13 +374,14 @@ public class BukkitConverters {
|
||||
* @return Item stack converter.
|
||||
*/
|
||||
public static EquivalentConverter<ItemStack> getItemStackConverter() {
|
||||
return getIgnoreNull(new EquivalentConverter<ItemStack>() {
|
||||
public Object getGeneric(Class<?> genericType, ItemStack specific) {
|
||||
return new IgnoreNullConverter<ItemStack>() {
|
||||
@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<ItemStack> getSpecificType() {
|
||||
return ItemStack.class;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -308,20 +399,15 @@ public class BukkitConverters {
|
||||
*/
|
||||
public static <TType> EquivalentConverter<TType> getIgnoreNull(final EquivalentConverter<TType> delegate) {
|
||||
// Automatically wrap all parameters to the delegate with a NULL check
|
||||
return new EquivalentConverter<TType>() {
|
||||
public Object getGeneric(Class<?> genericType, TType specific) {
|
||||
if (specific != null)
|
||||
return new IgnoreNullConverter<TType>() {
|
||||
@Override
|
||||
public Object getGenericValue(Class<?> genericType, TType specific) {
|
||||
return delegate.getGeneric(genericType, specific);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TType getSpecific(Object generic) {
|
||||
if (generic != null)
|
||||
public TType getSpecificValue(Object generic) {
|
||||
return delegate.getSpecific(generic);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -551,8 +551,17 @@ public class WrappedDataWatcher implements Iterable<WrappedWatchableObject> {
|
||||
List<Method> 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<WrappedWatchableObject> {
|
||||
} else {
|
||||
throw new IllegalStateException("Unable to find create and update watchable object. Update ProtocolLib.");
|
||||
}
|
||||
}
|
||||
|
||||
// Load the get-method
|
||||
// Be a little scientist - see if this in fact IS the right way around
|
||||
try {
|
||||
getKeyValueMethod = fuzzy.getMethodByParameters(
|
||||
"getWatchableObject", ".*WatchableObject", new String[] { int.class.getName() });
|
||||
getKeyValueMethod.setAccessible(true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Use fallback method
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
In neuem Issue referenzieren
Einen Benutzer sperren