Add reflection implementation structure

This commit is contained in:
Flowsqy 2021-12-29 17:12:27 +01:00
parent 278b4462f3
commit fe27bd5e60
8 changed files with 665 additions and 0 deletions

View File

@ -23,6 +23,11 @@
<artifactId>shopchest-nms-interface</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.inventivetalent</groupId>
<artifactId>reflectionhelper</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package de.epiceric.shopchest.nms.reflection;
import de.epiceric.shopchest.nms.FakeArmorStand;
import org.bukkit.Location;
import org.bukkit.entity.Player;
public class FakeArmorStandImpl extends FakeEntityImpl implements FakeArmorStand {
public FakeArmorStandImpl(ShopChestDebug debug) {
super(debug);
}
@Override
public void sendData(String name, Iterable<Player> receivers) {
}
@Override
public void setLocation(Location location, Iterable<Player> receivers) {
}
}

View File

@ -0,0 +1,33 @@
package de.epiceric.shopchest.nms.reflection;
import de.epiceric.shopchest.nms.FakeEntity;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import java.util.UUID;
public class FakeEntityImpl implements FakeEntity {
protected final int entityId;
protected final ShopChestDebug debug;
public FakeEntityImpl(ShopChestDebug debug) {
this.entityId = ReflectionUtils.getFreeEntityId();
this.debug = debug;
}
@Override
public int getEntityId() {
return entityId;
}
@Override
public void spawn(UUID uuid, Location location, Iterable<Player> receivers) {
}
@Override
public void remove(Iterable<Player> receivers) {
}
}

View File

@ -0,0 +1,22 @@
package de.epiceric.shopchest.nms.reflection;
import de.epiceric.shopchest.nms.FakeItem;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
public class FakeItemImpl extends FakeEntityImpl implements FakeItem {
public FakeItemImpl(ShopChestDebug debug) {
super(debug);
}
@Override
public void sendData(ItemStack item, Iterable<Player> receivers) {
}
@Override
public void resetVelocity(Iterable<Player> receivers) {
}
}

View File

@ -0,0 +1,243 @@
package de.epiceric.shopchest.nms.reflection;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import org.inventivetalent.reflection.resolver.FieldResolver;
import org.inventivetalent.reflection.resolver.minecraft.NMSClassResolver;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JsonBuilder {
public static class Part {
private String value;
public Part() {
this("", true);
}
public Part(Object value) {
this(value, value instanceof CharSequence);
}
public Part(Object value, boolean appendQuotes) {
if (appendQuotes) {
this.value = "\"" + value + "\"";
} else {
this.value = String.valueOf(value);
}
}
@Override
public String toString() {
return value;
}
public PartArray toArray() {
return new PartArray(this);
}
public PartMap toMap() {
PartMap map = new PartMap();
map.setValue("text", new Part());
map.setValue("extra", toArray());
return map;
}
}
public static class PartMap extends Part {
private Map<String, Part> values = new HashMap<>();
public PartMap() {
}
public PartMap(Map<String, Part> values) {
this.values.putAll(values);
}
public void setValue(String key, Part value) {
values.put(key, value);
}
public void removeValue(String key) {
values.remove(key);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(",", "{", "}");
values.forEach((key, value) -> joiner.add("\"" + key + "\":" + value.toString()));
return joiner.toString();
}
@Override
public PartMap toMap() {
return this;
}
}
public static class PartArray extends Part {
private List<Part> parts = new ArrayList<>();
public PartArray(Part... parts) {
this.parts.addAll(Arrays.asList(parts));
}
public void addPart(Part part) {
parts.add(part);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(",", "[", "]");
parts.forEach(part -> joiner.add(part.toString()));
return joiner.toString();
}
@Override
public PartArray toArray() {
return this;
}
}
private static final Pattern PART_PATTERN = Pattern.compile("((§(?:[a-fA-Fk-oK-OrR0-9]|#[a-fA-F0-9]{6}))+)([^§]*)");
private static final Pattern HEX_PATTERN = Pattern.compile("(§[a-fA-F0-9]){6}");
private Part rootPart;
private ShopChestDebug debug;
private final NMSClassResolver nmsClassResolver = new NMSClassResolver();
private Class<?> iChatBaseComponentClass = nmsClassResolver.resolveSilent("network.chat.IChatBaseComponent");
private Class<?> packetPlayOutChatClass = nmsClassResolver.resolveSilent("network.protocol.game.PacketPlayOutChat");
private Class<?> chatSerializerClass = nmsClassResolver.resolveSilent("ChatSerializer", "network.chat.IChatBaseComponent$ChatSerializer");
private Class<?> chatMessageTypeClass;
public JsonBuilder(ShopChestDebug debug) {
this.debug = debug;
if (ReflectionUtils.getMajorVersion() >= 16) {
chatMessageTypeClass = nmsClassResolver.resolveSilent("network.chat.ChatMessageType");
}
Class<?>[] requiredClasses = new Class<?>[] {
iChatBaseComponentClass, packetPlayOutChatClass, chatSerializerClass
};
for (Class<?> c : requiredClasses) {
if (c == null) {
debug.debug("Failed to instantiate JsonBuilder: Could not find all required classes");
return;
}
}
}
public static Part parse(String text) {
Matcher hexMatcher = HEX_PATTERN.matcher(text);
while (hexMatcher.find()) {
String hexCode = hexMatcher.group(0).replace("§", "");
text = text.replace(hexMatcher.group(0), "§#" + hexCode);
}
Matcher matcher = PART_PATTERN.matcher(text);
if (!matcher.find()) {
return new Part(text);
}
matcher.reset();
PartArray array = new PartArray(new Part());
int lastEndIndex = 0;
while (matcher.find()) {
int startIndex = matcher.start();
int endIndex = matcher.end();
if (lastEndIndex != startIndex) {
String betweenMatches = text.substring(lastEndIndex, startIndex);
array.addPart(new Part(betweenMatches));
}
String format = matcher.group(1);
String value = matcher.group(3);
PartMap part = new PartMap();
part.setValue("text", new Part(value));
String[] formats = format.split("§");
for (String f : formats) {
switch (f.toLowerCase()) {
case "":
break;
case "k":
part.setValue("obuscated", new Part(true));
break;
case "l":
part.setValue("bold", new Part(true));
break;
case "m":
part.setValue("strikethrough", new Part(true));
break;
case "n":
part.setValue("underlined", new Part(true));
break;
case "o":
part.setValue("italic", new Part(true));
break;
case "r":
part.removeValue("obfuscated");
part.removeValue("bold");
part.removeValue("strikethrough");
part.removeValue("underlined");
part.removeValue("italic");
part.removeValue("color");
break;
default:
if (f.startsWith("#")) {
part.setValue("color", new Part(f));
} else {
part.setValue("color", new Part(ChatColor.getByChar(f).name().toLowerCase()));
}
}
}
array.addPart(part);
lastEndIndex = endIndex;
}
return array;
}
@Override
public String toString() {
return rootPart.toString();
}
public Part getRootPart() {
return rootPart;
}
public void setRootPart(Part rootPart) {
this.rootPart = rootPart;
}
public void sendJson(Player p) {
try {
Object iChatBaseComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, toString());
Object packetPlayOutChat = ReflectionUtils.getMajorVersion() < 16
? packetPlayOutChatClass.getConstructor(iChatBaseComponentClass).newInstance(iChatBaseComponent)
: packetPlayOutChatClass.getConstructor(iChatBaseComponentClass, chatMessageTypeClass, UUID.class)
.newInstance(iChatBaseComponent, (new FieldResolver(chatMessageTypeClass)).resolve("CHAT", "a").get(null), UUID.randomUUID());
ReflectionUtils.sendPacket(debug, packetPlayOutChat, p);
debug.debug("Sent JSON: " + toString());
} catch (ReflectiveOperationException e) {
debug.getLogger().severe("Failed to send JSON with reflection");
debug.debug("Failed to send JSON with reflection: " + toString());
debug.debug(e);
}
}
}

View File

@ -0,0 +1,25 @@
package de.epiceric.shopchest.nms.reflection;
import de.epiceric.shopchest.nms.FakeArmorStand;
import de.epiceric.shopchest.nms.FakeItem;
import de.epiceric.shopchest.nms.Platform;
public class PlatformImpl implements Platform {
private final ShopChestDebug debug;
public PlatformImpl(ShopChestDebug debug) {
this.debug = debug;
}
@Override
public FakeArmorStand createFakeArmorStand() {
return new FakeArmorStandImpl(debug);
}
@Override
public FakeItem createFakeItem() {
return new FakeItemImpl(debug);
}
}

View File

@ -0,0 +1,285 @@
package de.epiceric.shopchest.nms.reflection;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.inventivetalent.reflection.resolver.FieldResolver;
import org.inventivetalent.reflection.resolver.minecraft.NMSClassResolver;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
public class ReflectionUtils {
static NMSClassResolver nmsClassResolver = new NMSClassResolver();
static Class<?> entityClass = nmsClassResolver.resolveSilent("world.entity.Entity");
static Class<?> entityArmorStandClass = nmsClassResolver.resolveSilent("world.entity.decoration.EntityArmorStand");
static Class<?> entityItemClass = nmsClassResolver.resolveSilent("world.entity.item.EntityItem");
static Class<?> dataWatcherClass = nmsClassResolver.resolveSilent("network.syncher.DataWatcher");
static Class<?> dataWatcherObjectClass = nmsClassResolver.resolveSilent("network.syncher.DataWatcherObject");
static Class<?> chatSerializerClass = nmsClassResolver.resolveSilent("ChatSerializer", "network.chat.IChatBaseComponent$ChatSerializer");
private ReflectionUtils() {}
/**
* Create a NMS data watcher object to send via a {@code PacketPlayOutEntityMetadata} packet.
* Gravity will be disabled and the custom name will be displayed if available.
* @param customName Custom Name of the entity or {@code null}
* @param nmsItemStack NMS ItemStack or {@code null} if armor stand
*/
public static Object createDataWatcher(ShopChestDebug debug, String customName, Object nmsItemStack) {
String version = getServerVersion();
int majorVersion = getMajorVersion();
try {
byte entityFlags = nmsItemStack == null ? (byte) 0b100000 : 0; // invisible if armor stand
byte armorStandFlags = nmsItemStack == null ? (byte) 0b10000 : 0; // marker (since 1.8_R2)
Object dataWatcher = dataWatcherClass.getConstructor(entityClass).newInstance((Object) null);
if (majorVersion < 9) {
if (getRevision() == 1) armorStandFlags = 0; // Marker not supported on 1.8_R1
Method a = dataWatcherClass.getMethod("a", int.class, Object.class);
a.invoke(dataWatcher, 0, entityFlags); // flags
a.invoke(dataWatcher, 1, (short) 300); // air ticks (?)
a.invoke(dataWatcher, 3, (byte) (customName != null ? 1 : 0)); // custom name visible
a.invoke(dataWatcher, 2, customName != null ? customName : ""); // custom name
a.invoke(dataWatcher, 4, (byte) 1); // silent
a.invoke(dataWatcher, 10, nmsItemStack == null ? armorStandFlags : nmsItemStack); // item / armor stand flags
} else {
Method register = dataWatcherClass.getMethod("register", dataWatcherObjectClass, Object.class);
String[] dataWatcherObjectFieldNames;
if ("v1_9_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"ax", "ay", "aA", "az", "aB", null, "c", "a"};
} else if ("v1_9_R2".equals(version)){
dataWatcherObjectFieldNames = new String[] {"ay", "az", "aB", "aA", "aC", null, "c", "a"};
} else if ("v1_10_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"aa", "az", "aB", "aA", "aC", "aD", "c", "a"};
} else if ("v1_11_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"Z", "az", "aB", "aA", "aC", "aD", "c", "a"};
} else if ("v1_12_R1".equals(version) || "v1_12_R2".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"Z", "aA", "aC", "aB", "aD", "aE", "c", "a"};
} else if ("v1_13_R1".equals(version) || "v1_13_R2".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"ac", "aD", "aF", "aE", "aG", "aH", "b", "a"};
} else if ("v1_14_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"W", "AIR_TICKS", "aA", "az", "aB", "aC", "ITEM", "b"};
} else if ("v1_15_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"T", "AIR_TICKS", "aA", "az", "aB", "aC", "ITEM", "b"};
} else if ("v1_16_R1".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"T", "AIR_TICKS", "ay", "ax", "az", "aA", "ITEM", "b"};
} else if ("v1_16_R2".equals(version) || "v1_16_R3".equals(version)) {
dataWatcherObjectFieldNames = new String[] {"S", "AIR_TICKS", "ar", "aq", "as", "at", "ITEM", "b"};
} else {
return null;
}
Field fEntityFlags = entityClass.getDeclaredField(dataWatcherObjectFieldNames[0]);
Field fAirTicks = entityClass.getDeclaredField(dataWatcherObjectFieldNames[1]);
Field fNameVisible = entityClass.getDeclaredField(dataWatcherObjectFieldNames[2]);
Field fCustomName = entityClass.getDeclaredField(dataWatcherObjectFieldNames[3]);
Field fSilent = entityClass.getDeclaredField(dataWatcherObjectFieldNames[4]);
Field fNoGravity = majorVersion >= 10 ? entityClass.getDeclaredField(dataWatcherObjectFieldNames[5]) : null;
Field fItem = entityItemClass.getDeclaredField(dataWatcherObjectFieldNames[6]);
Field fArmorStandFlags = entityArmorStandClass.getDeclaredField(dataWatcherObjectFieldNames[7]);
fEntityFlags.setAccessible(true);
fAirTicks.setAccessible(true);
fNameVisible.setAccessible(true);
fCustomName.setAccessible(true);
fSilent.setAccessible(true);
if (majorVersion >= 10) fNoGravity.setAccessible(true);
fItem.setAccessible(true);
fArmorStandFlags.setAccessible(true);
register.invoke(dataWatcher, fEntityFlags.get(null), entityFlags);
register.invoke(dataWatcher, fAirTicks.get(null), 300);
register.invoke(dataWatcher, fNameVisible.get(null), customName != null);
register.invoke(dataWatcher, fSilent.get(null), true);
if (majorVersion < 13) register.invoke(dataWatcher, fCustomName.get(null), customName != null ? customName : "");
if (nmsItemStack != null) {
register.invoke(dataWatcher, fItem.get(null), majorVersion < 11 ? com.google.common.base.Optional.of(nmsItemStack) : nmsItemStack);
} else {
register.invoke(dataWatcher, fArmorStandFlags.get(null), armorStandFlags);
}
if (majorVersion >= 10) {
register.invoke(dataWatcher, fNoGravity.get(null), true);
if (majorVersion >= 13) {
if (customName != null) {
Object iChatBaseComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, JsonBuilder.parse(customName).toString());
register.invoke(dataWatcher, fCustomName.get(null), Optional.of(iChatBaseComponent));
} else {
register.invoke(dataWatcher, fCustomName.get(null), Optional.empty());
}
}
}
}
return dataWatcher;
} catch (InstantiationException | InvocationTargetException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) {
debug.getLogger().severe("Failed to create data watcher!");
debug.debug("Failed to create data watcher");
debug.debug(e);
}
return null;
}
/**
* Get a free entity ID for use in {@link #createPacketSpawnEntity(ShopChestDebug, int, UUID, Location, EntityType)}
*
* @return The id or {@code -1} if a free entity ID could not be retrieved.
*/
public static int getFreeEntityId() {
try {
Field entityCountField = new FieldResolver(entityClass).resolve("entityCount", "b");
entityCountField.setAccessible(true);
if (entityCountField.getType() == int.class) {
int id = entityCountField.getInt(null);
entityCountField.setInt(null, id+1);
return id;
} else if (entityCountField.getType() == AtomicInteger.class) {
return ((AtomicInteger) entityCountField.get(null)).incrementAndGet();
}
return -1;
} catch (Exception e) {
return -1;
}
}
/**
* Create a {@code PacketPlayOutSpawnEntity} object.
* Only {@link EntityType#ARMOR_STAND} and {@link EntityType#DROPPED_ITEM} are supported!
*/
public static Object createPacketSpawnEntity(ShopChestDebug debug, int id, UUID uuid, Location loc, EntityType type) {
try {
Class<?> packetPlayOutSpawnEntityClass = nmsClassResolver.resolveSilent("network.protocol.game.PacketPlayOutSpawnEntity");
Class<?> entityTypesClass = nmsClassResolver.resolveSilent("world.entity.EntityTypes");
boolean isPre9 = getMajorVersion() < 9;
boolean isPre14 = getMajorVersion() < 14;
double y = loc.getY();
if (type == EntityType.ARMOR_STAND && !getServerVersion().equals("v1_8_R1")) {
// Marker armor stand => lift by normal armor stand height
y += 1.975;
}
Object packet = packetPlayOutSpawnEntityClass.getConstructor().newInstance();
Field[] fields = new Field[12];
fields[0] = packetPlayOutSpawnEntityClass.getDeclaredField("a"); // ID
fields[1] = packetPlayOutSpawnEntityClass.getDeclaredField("b"); // UUID (Only 1.9+)
fields[2] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "b" : "c"); // Loc X
fields[3] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "c" : "d"); // Loc Y
fields[4] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "d" : "e"); // Loc Z
fields[5] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "e" : "f"); // Mot X
fields[6] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "f" : "g"); // Mot Y
fields[7] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "g" : "h"); // Mot Z
fields[8] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "h" : "i"); // Pitch
fields[9] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "i" : "j"); // Yaw
fields[10] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "j" : "k"); // Type
fields[11] = packetPlayOutSpawnEntityClass.getDeclaredField(isPre9 ? "k" : "l"); // Data
for (Field field : fields) {
field.setAccessible(true);
}
Object entityType = null;
if (!isPre14) {
entityType = entityTypesClass.getField(type == EntityType.ARMOR_STAND ? "ARMOR_STAND" : "ITEM").get(null);
}
fields[0].set(packet, id);
if (!isPre9) fields[1].set(packet, uuid);
if (isPre9) {
fields[2].set(packet, (int)(loc.getX() * 32));
fields[3].set(packet, (int)(y * 32));
fields[4].set(packet, (int)(loc.getZ() * 32));
} else {
fields[2].set(packet, loc.getX());
fields[3].set(packet, y);
fields[4].set(packet, loc.getZ());
}
fields[5].set(packet, 0);
fields[6].set(packet, 0);
fields[7].set(packet, 0);
fields[8].set(packet, 0);
fields[9].set(packet, 0);
if (isPre14) fields[10].set(packet, type == EntityType.ARMOR_STAND ? 78 : 2);
else fields[10].set(packet, entityType);
fields[11].set(packet, 0);
return packet;
} catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
debug.getLogger().severe("Failed to create packet to spawn entity!");
debug.debug("Failed to create packet to spawn entity!");
debug.debug(e);
return null;
}
}
/**
* Send a packet to a player
* @param debug An instance of the {@link ShopChestDebug} debug instance
* @param packet Packet to send
* @param player Player to which the packet should be sent
* @return {@code true} if the packet was sent, or {@code false} if an exception was thrown
*/
public static boolean sendPacket(ShopChestDebug debug, Object packet, Player player) {
try {
if (packet == null) {
debug.debug("Failed to send packet: Packet is null");
return false;
}
Class<?> packetClass = nmsClassResolver.resolveSilent("network.protocol.Packet");
if (packetClass == null) {
debug.debug("Failed to send packet: Could not find Packet class");
return false;
}
Object nmsPlayer = player.getClass().getMethod("getHandle").invoke(player);
Field fConnection = (new FieldResolver(nmsPlayer.getClass())).resolve("playerConnection", "b");
Object playerConnection = fConnection.get(nmsPlayer);
playerConnection.getClass().getMethod("sendPacket", packetClass).invoke(playerConnection, packet);
return true;
} catch (NoSuchMethodException | NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
debug.getLogger().severe("Failed to send packet " + packet.getClass().getName());
debug.debug("Failed to send packet " + packet.getClass().getName());
debug.debug(e);
return false;
}
}
/**
* @return The current server version with revision number (e.g. v1_9_R2, v1_10_R1)
*/
public static String getServerVersion() {
String packageName = Bukkit.getServer().getClass().getPackage().getName();
return packageName.substring(packageName.lastIndexOf('.') + 1);
}
/**
* @return The revision of the current server version (e.g. <i>2</i> for v1_9_R2, <i>1</i> for v1_10_R1)
*/
public static int getRevision() {
return Integer.parseInt(getServerVersion().substring(getServerVersion().length() - 1));
}
/**
* @return The major version of the server (e.g. <i>9</i> for 1.9.2, <i>10</i> for 1.10)
*/
public static int getMajorVersion() {
return Integer.parseInt(getServerVersion().split("_")[1]);
}
}

View File

@ -0,0 +1,30 @@
package de.epiceric.shopchest.nms.reflection;
import java.util.function.Consumer;
import java.util.logging.Logger;
public class ShopChestDebug {
private final Logger logger;
private final Consumer<String> debugConsumer;
private final Consumer<Throwable> throwableConsumer;
public ShopChestDebug(Logger logger, Consumer<String> debugConsumer, Consumer<Throwable> throwableConsumer) {
this.logger = logger;
this.debugConsumer = debugConsumer;
this.throwableConsumer = throwableConsumer;
}
public Logger getLogger() {
return logger;
}
public void debug(String message){
debugConsumer.accept(message);
}
public void debug(Throwable e){
throwableConsumer.accept(e);
}
}