313 lines
13 KiB
Java
313 lines
13 KiB
Java
/*
|
|
* 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;
|
|
|
|
import com.comphenix.protocol.reflect.FieldAccessException;
|
|
import com.comphenix.protocol.reflect.FuzzyReflection;
|
|
import com.comphenix.protocol.reflect.accessors.Accessors;
|
|
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
|
|
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
|
|
import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract;
|
|
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
|
|
import com.comphenix.protocol.utility.MinecraftFields;
|
|
import com.comphenix.protocol.utility.MinecraftReflection;
|
|
import com.comphenix.protocol.utility.MinecraftVersion;
|
|
import com.comphenix.protocol.utility.Util;
|
|
import com.comphenix.protocol.wrappers.WrappedIntHashMap;
|
|
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Modifier;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
import org.apache.commons.lang.Validate;
|
|
import org.bukkit.World;
|
|
import org.bukkit.entity.Entity;
|
|
import org.bukkit.entity.Player;
|
|
|
|
/**
|
|
* Used to perform certain operations on entities.
|
|
*
|
|
* @author Kristian
|
|
*/
|
|
class EntityUtilities {
|
|
|
|
private static final EntityUtilities INSTANCE = new EntityUtilities();
|
|
private static final boolean NEW_TRACKER = MinecraftVersion.VILLAGE_UPDATE.atOrAbove();
|
|
|
|
private final Map<Class<?>, MethodAccessor> scanPlayersMethods = new ConcurrentHashMap<>();
|
|
private final Map<Class<?>, FieldAccessor> trackedEntityFields = new ConcurrentHashMap<>();
|
|
|
|
private FieldAccessor chunkMapField;
|
|
private FieldAccessor entityTrackerField;
|
|
private FieldAccessor trackedPlayersField;
|
|
private FieldAccessor trackedEntitiesField;
|
|
private FieldAccessor foliaTrackerField;
|
|
|
|
private MethodAccessor getEntity;
|
|
private MethodAccessor getChunkProvider;
|
|
|
|
private EntityUtilities() {
|
|
}
|
|
|
|
public static EntityUtilities getInstance() {
|
|
return INSTANCE;
|
|
}
|
|
|
|
public void updateEntity(Entity entity, List<Player> observers) {
|
|
if (entity == null || !entity.isValid()) {
|
|
return;
|
|
}
|
|
|
|
Collection<?> trackedPlayers = this.getTrackedPlayers(entity);
|
|
List<Object> nmsPlayers = this.unwrapBukkit(observers);
|
|
|
|
List<Object> removingEntries =
|
|
MinecraftVersion.CAVES_CLIFFS_1.atOrAbove() ? this.getPlayerConnections(nmsPlayers) : nmsPlayers;
|
|
trackedPlayers.removeAll(removingEntries);
|
|
|
|
// there can be multiple different entity tracker entry impls, see GH-732....
|
|
Object trackerEntry = this.getEntityTrackerEntry(entity.getWorld(), entity.getEntityId());
|
|
this.scanPlayersMethods.computeIfAbsent(trackerEntry.getClass(), this::findScanPlayers)
|
|
.invoke(trackerEntry, nmsPlayers);
|
|
}
|
|
|
|
public Entity getEntity(World world, int id) {
|
|
// new tracker registers the entity before sending out any packets - we can use the supported getter method here
|
|
if (NEW_TRACKER) {
|
|
Object level = BukkitUnwrapper.getInstance().unwrapItem(world);
|
|
if (this.getEntity == null) {
|
|
Method entityGetter = FuzzyReflection.fromObject(level).getMethodByReturnTypeAndParameters(
|
|
"getEntity",
|
|
MinecraftReflection.getEntityClass(),
|
|
int.class);
|
|
this.getEntity = Accessors.getMethodAccessor(entityGetter);
|
|
}
|
|
|
|
Object entity = this.getEntity.invoke(level, id);
|
|
return (Entity) MinecraftReflection.getBukkitEntity(entity);
|
|
}
|
|
|
|
// old tracker - now the pain begins
|
|
Object trackerEntry = this.getEntityTrackerEntry(world, id);
|
|
if (trackerEntry == null) {
|
|
// not tracked
|
|
return null;
|
|
}
|
|
|
|
FieldAccessor trackedEntityAccessor = this.trackedEntityFields.computeIfAbsent(trackerEntry.getClass(), clz -> {
|
|
try {
|
|
// try to get the first entity field from the tracker class
|
|
Field entityField = FuzzyReflection.fromClass(clz, true).getField(FuzzyFieldContract.newBuilder()
|
|
.typeExact(MinecraftReflection.getEntityClass())
|
|
.build());
|
|
return Accessors.getFieldAccessor(entityField);
|
|
} catch (IllegalArgumentException exception) {
|
|
// no such field, try the base tracker class instead
|
|
Class<?> trackerClass = MinecraftReflection.getEntityTrackerClass();
|
|
Field entityField = FuzzyReflection.fromClass(trackerClass, true).getField(FuzzyFieldContract.newBuilder()
|
|
.typeExact(MinecraftReflection.getEntityClass())
|
|
.build());
|
|
return Accessors.getFieldAccessor(entityField);
|
|
}
|
|
});
|
|
|
|
Object entity = trackedEntityAccessor.get(trackerEntry);
|
|
return (Entity) MinecraftReflection.getBukkitEntity(entity);
|
|
}
|
|
|
|
private MethodAccessor findScanPlayers(Class<?> trackerClass) {
|
|
MethodAccessor candidate = Accessors.getMethodAccessorOrNull(trackerClass, "scanPlayers");
|
|
if (candidate != null) {
|
|
return candidate;
|
|
}
|
|
|
|
FuzzyReflection fuzzy = FuzzyReflection.fromClass(trackerClass, true);
|
|
return Accessors.getMethodAccessor(fuzzy.getMethod(FuzzyMethodContract.newBuilder()
|
|
.returnTypeVoid()
|
|
.parameterExactArray(List.class)
|
|
.build()));
|
|
}
|
|
|
|
/**
|
|
* Retrieve every client that is receiving information about a given entity.
|
|
*
|
|
* @param entity - the entity that is being tracked.
|
|
* @return Every client/player that is tracking the given entity.
|
|
* @throws FieldAccessException If reflection failed.
|
|
*/
|
|
public List<Player> getEntityTrackers(Entity entity) {
|
|
if (entity == null || !entity.isValid()) {
|
|
return new ArrayList<>();
|
|
}
|
|
|
|
List<Player> result = new ArrayList<>();
|
|
Collection<?> trackedPlayers = this.getTrackedPlayers(entity);
|
|
|
|
// Wrap every player - we also ensure that the underlying tracker list is immutable
|
|
for (Object tracker : trackedPlayers) {
|
|
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove() && MinecraftReflection.isServerHandler(tracker)) {
|
|
result.add(MinecraftReflection.getBukkitPlayerFromConnection(tracker));
|
|
} else if (MinecraftReflection.isMinecraftPlayer(tracker)) {
|
|
result.add((Player) MinecraftReflection.getBukkitEntity(tracker));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private Collection<?> getTrackedPlayers(Entity entity) {
|
|
Validate.notNull(entity, "entity cannot be null");
|
|
|
|
Object trackerEntry = this.getEntityTrackerEntry(entity.getWorld(), entity.getEntityId());
|
|
if (trackerEntry == null) { //entity.toString() is too expensive to be called every time
|
|
throw new IllegalArgumentException("Could not find entity trackers for " + entity);
|
|
}
|
|
|
|
if (this.trackedPlayersField == null) {
|
|
this.trackedPlayersField = Accessors.getFieldAccessor(
|
|
FuzzyReflection.fromObject(trackerEntry).getFieldByType("java\\.util\\..*"));
|
|
}
|
|
|
|
Validate.notNull(this.trackedPlayersField, "Could not find trackedPlayers field");
|
|
|
|
Object value = this.trackedPlayersField.get(trackerEntry);
|
|
if (value instanceof Collection) {
|
|
return (Collection<?>) value;
|
|
} else if (value instanceof Map) {
|
|
return ((Map<?, ?>) value).keySet();
|
|
} else {
|
|
// Please. No more changes.
|
|
throw new IllegalStateException(
|
|
"trackedPlayers field was an unknown type: expected Collection or Map, but got " + value.getClass());
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private Object getNewEntityTracker(Object worldServer, int entityId) {
|
|
if (Util.isUsingFolia()) {
|
|
if (this.getEntity == null) {
|
|
Method entityGetter = FuzzyReflection.fromObject(worldServer).getMethodByReturnTypeAndParameters(
|
|
"getEntity",
|
|
MinecraftReflection.getEntityClass(),
|
|
int.class);
|
|
this.getEntity = Accessors.getMethodAccessor(entityGetter);
|
|
}
|
|
|
|
Object entity = this.getEntity.invoke(worldServer, entityId);
|
|
|
|
if (this.foliaTrackerField == null) {
|
|
this.foliaTrackerField = Accessors.getFieldAccessor(FuzzyReflection.fromClass(entity.getClass(), false)
|
|
.getField(FuzzyFieldContract.newBuilder().typeExact(MinecraftReflection.getEntityTrackerClass()).build()));
|
|
}
|
|
|
|
return this.foliaTrackerField.get(entity);
|
|
}
|
|
|
|
if (this.getChunkProvider == null) {
|
|
Class<?> chunkProviderClass = MinecraftReflection.getChunkProviderServer();
|
|
this.getChunkProvider = Accessors.getMethodAccessor(FuzzyReflection.fromClass(worldServer.getClass(), false)
|
|
.getMethod(FuzzyMethodContract.newBuilder().parameterCount(0).returnTypeExact(chunkProviderClass).build()));
|
|
}
|
|
|
|
Object chunkProvider = this.getChunkProvider.invoke(worldServer);
|
|
|
|
if (this.chunkMapField == null) {
|
|
Class<?> chunkMapClass = MinecraftReflection.getPlayerChunkMap();
|
|
this.chunkMapField = Accessors.getFieldAccessor(FuzzyReflection.fromClass(chunkProvider.getClass(), false)
|
|
.getField(FuzzyFieldContract.newBuilder().typeExact(chunkMapClass).build()));
|
|
}
|
|
|
|
Object playerChunkMap = this.chunkMapField.get(chunkProvider);
|
|
|
|
if (this.trackedEntitiesField == null) {
|
|
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) {
|
|
this.trackedEntitiesField = Accessors.getFieldAccessor(
|
|
FuzzyReflection.fromClass(playerChunkMap.getClass(), true)
|
|
.getField(FuzzyFieldContract.newBuilder()
|
|
.banModifier(Modifier.STATIC)
|
|
.requirePublic()
|
|
.typeExact(MinecraftReflection.getInt2ObjectMapClass())
|
|
.build()));
|
|
} else {
|
|
this.trackedEntitiesField = Accessors.getFieldAccessor(
|
|
FuzzyReflection.fromClass(playerChunkMap.getClass(), false).getField(
|
|
FuzzyFieldContract.newBuilder().typeDerivedOf(Map.class).nameExact("trackedEntities").build()));
|
|
}
|
|
}
|
|
|
|
Map<Integer, Object> trackedEntities = (Map<Integer, Object>) this.trackedEntitiesField.get(playerChunkMap);
|
|
return trackedEntities.get(entityId);
|
|
}
|
|
|
|
private Object getEntityTrackerEntry(World world, int entityID) {
|
|
BukkitUnwrapper unwrapper = new BukkitUnwrapper();
|
|
Object worldServer = unwrapper.unwrapItem(world);
|
|
|
|
if (NEW_TRACKER) {
|
|
return this.getNewEntityTracker(worldServer, entityID);
|
|
}
|
|
|
|
if (this.entityTrackerField == null) {
|
|
this.entityTrackerField = Accessors.getFieldAccessor(FuzzyReflection.fromObject(worldServer).
|
|
getFieldByType("tracker", MinecraftReflection.getEntityTrackerClass()));
|
|
}
|
|
|
|
// Get the tracker
|
|
Object tracker = this.entityTrackerField.get(worldServer);
|
|
|
|
// Looking for an IntHashMap in the tracker entry
|
|
if (this.trackedEntitiesField == null) {
|
|
this.trackedEntitiesField = Accessors.getFieldAccessor(FuzzyReflection.fromObject(tracker, false)
|
|
.getFieldByType("trackedEntities", MinecraftReflection.getIntHashMapClass()));
|
|
}
|
|
|
|
// Read the map
|
|
Object trackedEntities = this.trackedEntitiesField.get(tracker);
|
|
return WrappedIntHashMap.fromHandle(trackedEntities).get(entityID);
|
|
}
|
|
|
|
private List<Object> getPlayerConnections(List<Object> nmsPlayers) {
|
|
List<Object> connections = new ArrayList<>(nmsPlayers.size());
|
|
nmsPlayers.forEach(nmsPlayer -> connections.add(MinecraftFields.getPlayerConnection(nmsPlayer)));
|
|
return connections;
|
|
}
|
|
|
|
private List<Object> unwrapBukkit(List<Player> players) {
|
|
final List<Object> output = new ArrayList<>();
|
|
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;
|
|
}
|
|
}
|