diff --git a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java index 337d13c2..48bf9846 100644 --- a/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/ProtocolManager.java @@ -18,14 +18,17 @@ package com.comphenix.protocol; import java.lang.reflect.InvocationTargetException; +import java.util.List; import java.util.Set; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.PacketConstructor; +import com.comphenix.protocol.reflect.FieldAccessException; import com.google.common.collect.ImmutableSet; /** @@ -133,6 +136,13 @@ public interface ProtocolManager { */ public PacketConstructor createPacketConstructor(int id, Object... arguments); + /** + * Completely refresh all clients about an entity. + * @param entity - entity to refresh. + * @param observers - the clients to update. + */ + public void updateEntity(Entity entity, List observers) throws FieldAccessException; + /** * Retrieves a immutable set containing the ID of the sent server packets that will be observed by listeners. * @return Every filtered server packet. diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java b/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java new file mode 100644 index 00000000..d358bcd7 --- /dev/null +++ b/ProtocolLib/src/com/comphenix/protocol/injector/EntityUtilities.java @@ -0,0 +1,159 @@ +package com.comphenix.protocol.injector; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.injector.PacketConstructor.BukkitUnwrapper; +import com.comphenix.protocol.reflect.FieldAccessException; +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.collect.Lists; + +/** + * Used to perform certain operations on entities. + * + * @author Kristian + */ +class EntityUtilities { + + private static Field entityTrackerField; + private static Field trackedEntitiesField; + private static Field trackedPlayersField; + + private static Method hashGetMethod; + private static Method scanPlayersMethod; + + public static void updateEntity(Entity entity, List observers) throws FieldAccessException { + + World world = entity.getWorld(); + Object worldServer = ((CraftWorld) world).getHandle(); + + // We have to rely on the class naming here. + if (entityTrackerField == null) + entityTrackerField = FuzzyReflection.fromObject(worldServer).getFieldByType(".*Tracker"); + + // Get the tracker + Object tracker = null; + + try { + tracker = FieldUtils.readField(entityTrackerField, worldServer, false); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access 'tracker' field due to security limitations.", e); + } + + if (trackedEntitiesField == null) { + @SuppressWarnings("rawtypes") + Set ignoredTypes = new HashSet(); + + // Well, this is more difficult. But we're looking for a Minecraft object that is not + // created by the constructor(s). + for (Constructor constructor : tracker.getClass().getConstructors()) { + for (Class type : constructor.getParameterTypes()) { + ignoredTypes.add(type); + } + } + + // The Minecraft field that's NOT filled in by the constructor + trackedEntitiesField = FuzzyReflection.fromObject(tracker, true). + getFieldByType(FuzzyReflection.MINECRAFT_OBJECT, ignoredTypes); + } + + // Read the entity hashmap + Object trackedEntities = null; + + try { + trackedEntities = FieldUtils.readField(trackedEntitiesField, tracker, true); + } catch (IllegalAccessException e) { + throw new FieldAccessException("Cannot access 'trackedEntities' field due to security limitations.", e); + } + + // Getting the "get" method is pretty hard, but first - try to just get it by name + if (hashGetMethod == null) { + + Class type = trackedEntities.getClass(); + + try { + hashGetMethod = type.getMethod("get", int.class); + } catch (NoSuchMethodException e) { + + Class[] params = { int.class }; + + // Then it's probably the lowest named method that takes an int-parameter + for (Method method : type.getMethods()) { + if (Arrays.equals(params, method.getParameterTypes())) { + if (hashGetMethod == null || + method.getName().compareTo(hashGetMethod.getName()) < 0) { + hashGetMethod = method; + } + } + } + } + } + + try { + //EntityTrackerEntry trackEntity = (EntityTrackerEntry) tracker.trackedEntities.get(entity.getEntityId()); + Object trackerEntry = hashGetMethod.invoke(trackedEntities, entity.getEntityId()); + + if (trackedPlayersField == null) { + // This one is fairly easy + trackedPlayersField = FuzzyReflection.fromObject(trackerEntry).getFieldByType("java\\.util\\..*"); + } + + // Phew, finally there. + Collection trackedPlayers = (Collection) FieldUtils.readField(trackedPlayersField, trackerEntry, false); + List nmsPlayers = unwrapBukkit(observers); + + // trackEntity.trackedPlayers.clear(); + trackedPlayers.removeAll(nmsPlayers); + + // We have to rely on a NAME once again. Damn it. + if (scanPlayersMethod == null) { + scanPlayersMethod = trackerEntry.getClass().getMethod("scanPlayers", List.class); + } + + //trackEntity.scanPlayers(server.players); + scanPlayersMethod.invoke(trackerEntry, nmsPlayers); + + } catch (IllegalArgumentException e) { + throw e; + } catch (IllegalAccessException e) { + throw new FieldAccessException("Security limitation prevents access to 'get' method in IntHashMap", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Exception occurred in Minecraft.", e); + } catch (SecurityException e) { + throw new FieldAccessException("Security limitation prevents access to 'scanPlayers' method in trackerEntry.", e); + } catch (NoSuchMethodException e) { + throw new FieldAccessException("Canot find 'scanPlayers' method. Is ProtocolLib up to date?", e); + } + } + + private static List unwrapBukkit(List players) { + + List output = Lists.newArrayList(); + BukkitUnwrapper unwrapper = new BukkitUnwrapper(); + + // Get the NMS equivalent + for (Player player : players) { + Object result = unwrapper.unwrapItem(player); + + if (result != null) + output.add(result); + else + throw new IllegalArgumentException("Cannot unwrap item " + player); + } + + return output; + } +} diff --git a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java index bae212c8..72e8c601 100644 --- a/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java +++ b/ProtocolLib/src/com/comphenix/protocol/injector/PacketFilterManager.java @@ -34,6 +34,7 @@ import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -336,7 +337,7 @@ public final class PacketFilterManager implements ProtocolManager { return PacketConstructor.DEFAULT.withPacket(id, types); } - + @Override public Set getSendingFilters() { return ImmutableSet.copyOf(sendingFilters); @@ -347,6 +348,11 @@ public final class PacketFilterManager implements ProtocolManager { return ImmutableSet.copyOf(packetInjector.getPacketHandlers()); } + @Override + public void updateEntity(Entity entity, List observers) throws FieldAccessException { + EntityUtilities.updateEntity(entity, observers); + } + /** * Initialize the packet injection for every player. * @param players - list of players to inject. diff --git a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java index cc26d1db..362daeaa 100644 --- a/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java +++ b/ProtocolLib/src/com/comphenix/protocol/reflect/FuzzyReflection.java @@ -199,6 +199,37 @@ public class FuzzyReflection { typeRegex + " in " + source.getName()); } + /** + * Retrieves a field by type. + *

+ * Note that the type is matched using the full canonical representation, i.e.: + *

    + *
  • java.util.List
  • + *
  • net.comphenix.xp.ExperienceMod
  • + *
+ * @param typeRegex - regular expression that will match the field type. + * @param ignoredTypes - types to ignore. + * @return The first field with a type that matches the given regular expression. + */ + @SuppressWarnings("rawtypes") + public Field getFieldByType(String typeRegex, Set ignored) { + + Pattern match = Pattern.compile(typeRegex); + + // Like above, only here we test the field type + for (Field field : getFields()) { + Class type = field.getType(); + + if (!ignored.contains(type) && match.matcher(type.getName()).matches()) { + return field; + } + } + + // Looks like we're outdated. Too bad. + throw new RuntimeException("Unable to find a field with the type " + + typeRegex + " in " + source.getName()); + } + /** * Retrieves all private and public fields in declared order (after JDK 1.5). * @return Every field.