Skip to content

Commit

Permalink
Add support for 1.17 (#11)
Browse files Browse the repository at this point in the history
Co-authored-by: MrMicky <[email protected]>
  • Loading branch information
syldium and MrMicky-FR authored Jun 11, 2021
1 parent fbef18d commit bf99e9f
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 45 deletions.
114 changes: 75 additions & 39 deletions src/main/java/fr/mrmicky/fastboard/FastBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -39,6 +40,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;

/**
Expand All @@ -52,19 +54,23 @@
*/
public class FastBoard {

private static final Map<Class<?>, List<Field>> PACKETS = new HashMap<>(8);
private static final Map<Class<?>, Field[]> PACKETS = new HashMap<>(8);
private static final VersionType VERSION_TYPE;
// Packets and components
private static final Class<?> CHAT_COMPONENT_CLASS;
private static final Class<?> CHAT_FORMAT_ENUM;
private static final Object EMPTY_MESSAGE;
private static final Object RESET_FORMATTING;
private static final MethodHandle MESSAGE_FROM_STRING;
private static final MethodHandle PLAYER_CONNECTION;
private static final MethodHandle SEND_PACKET;
private static final MethodHandle PLAYER_GET_HANDLE;
// Scoreboard packets
private static final MethodHandle PACKET_SB_OBJ;
private static final MethodHandle PACKET_SB_DISPLAY_OBJ;
private static final MethodHandle PACKET_SB_SCORE;
private static final MethodHandle PACKET_SB_TEAM;
private static final FastReflection.PacketConstructor PACKET_SB_OBJ;
private static final FastReflection.PacketConstructor PACKET_SB_DISPLAY_OBJ;
private static final FastReflection.PacketConstructor PACKET_SB_SCORE;
private static final FastReflection.PacketConstructor PACKET_SB_TEAM;
private static final FastReflection.PacketConstructor PACKET_SB_SERIALIZABLE_TEAM;
// Scoreboard enums
private static final Class<?> ENUM_SB_HEALTH_DISPLAY;
private static final Class<?> ENUM_SB_ACTION;
Expand All @@ -75,60 +81,78 @@ public class FastBoard {
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType voidType = MethodType.methodType(void.class);

if (FastReflection.nmsOptionalClass("ScoreboardServer$Action").isPresent()) {
if (FastReflection.isRepackaged()) {
VERSION_TYPE = VersionType.V1_17;
} else if (FastReflection.nmsOptionalClass(null, "ScoreboardServer$Action").isPresent()) {
VERSION_TYPE = VersionType.V1_13;
} else if (FastReflection.nmsOptionalClass("IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent()) {
} else if (FastReflection.nmsOptionalClass(null, "IScoreboardCriteria$EnumScoreboardHealthDisplay").isPresent()) {
VERSION_TYPE = VersionType.V1_8;
} else {
VERSION_TYPE = VersionType.V1_7;
}

String gameProtocolPackage = "network.protocol.game";
Class<?> craftPlayerClass = FastReflection.obcClass("entity.CraftPlayer");
Class<?> craftChatMessageClass = FastReflection.obcClass("util.CraftChatMessage");
Class<?> entityPlayerClass = FastReflection.nmsClass("EntityPlayer");
Class<?> playerConnectionClass = FastReflection.nmsClass("PlayerConnection");
Class<?> packetClass = FastReflection.nmsClass("Packet");
Class<?> packetSbObjClass = FastReflection.nmsClass("PacketPlayOutScoreboardObjective");
Class<?> packetSbDisplayObjClass = FastReflection.nmsClass("PacketPlayOutScoreboardDisplayObjective");
Class<?> packetSbScoreClass = FastReflection.nmsClass("PacketPlayOutScoreboardScore");
Class<?> packetSbTeamClass = FastReflection.nmsClass("PacketPlayOutScoreboardTeam");
Class<?> entityPlayerClass = FastReflection.nmsClass("server.level", "EntityPlayer");
Class<?> playerConnectionClass = FastReflection.nmsClass("server.network", "PlayerConnection");
Class<?> packetClass = FastReflection.nmsClass("network.protocol", "Packet");
Class<?> packetSbObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardObjective");
Class<?> packetSbDisplayObjClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardDisplayObjective");
Class<?> packetSbScoreClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardScore");
Class<?> packetSbTeamClass = FastReflection.nmsClass(gameProtocolPackage, "PacketPlayOutScoreboardTeam");
Class<?> sbTeamClass = VersionType.V1_17.isHigherOrEqual()
? FastReflection.innerClass(packetSbTeamClass, innerClass -> !innerClass.isEnum()) : null;
Field playerConnectionField = Arrays.stream(entityPlayerClass.getFields())
.filter(field -> field.getType().isAssignableFrom(playerConnectionClass))
.findFirst().orElseThrow(NoSuchFieldException::new);

MESSAGE_FROM_STRING = lookup.unreflect(craftChatMessageClass.getMethod("fromString", String.class));
CHAT_COMPONENT_CLASS = FastReflection.nmsClass("IChatBaseComponent");
CHAT_COMPONENT_CLASS = FastReflection.nmsClass("network.chat", "IChatBaseComponent");
CHAT_FORMAT_ENUM = FastReflection.nmsClass(null, "EnumChatFormat");
EMPTY_MESSAGE = Array.get(MESSAGE_FROM_STRING.invoke(""), 0);
RESET_FORMATTING = FastReflection.enumValueOf(CHAT_FORMAT_ENUM, "RESET", 21);
PLAYER_GET_HANDLE = lookup.findVirtual(craftPlayerClass, "getHandle", MethodType.methodType(entityPlayerClass));
PLAYER_CONNECTION = lookup.findGetter(entityPlayerClass, "playerConnection", playerConnectionClass);
PLAYER_CONNECTION = lookup.unreflectGetter(playerConnectionField);
SEND_PACKET = lookup.findVirtual(playerConnectionClass, "sendPacket", MethodType.methodType(void.class, packetClass));
PACKET_SB_OBJ = lookup.findConstructor(packetSbObjClass, voidType);
PACKET_SB_DISPLAY_OBJ = lookup.findConstructor(packetSbDisplayObjClass, voidType);
PACKET_SB_SCORE = lookup.findConstructor(packetSbScoreClass, voidType);
PACKET_SB_TEAM = lookup.findConstructor(packetSbTeamClass, voidType);

for (Class<?> clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass)) {
List<Field> fields = Arrays.asList(clazz.getDeclaredFields());
fields.forEach(field -> field.setAccessible(true));
PACKET_SB_OBJ = FastReflection.findPacketConstructor(packetSbObjClass, lookup);
PACKET_SB_DISPLAY_OBJ = FastReflection.findPacketConstructor(packetSbDisplayObjClass, lookup);
PACKET_SB_SCORE = FastReflection.findPacketConstructor(packetSbScoreClass, lookup);
PACKET_SB_TEAM = FastReflection.findPacketConstructor(packetSbTeamClass, lookup);
PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass == null ? null : FastReflection.findPacketConstructor(sbTeamClass, lookup);

for (Class<?> clazz : Arrays.asList(packetSbObjClass, packetSbDisplayObjClass, packetSbScoreClass, packetSbTeamClass, sbTeamClass)) {
if (clazz == null) {
continue;
}
Field[] fields = Arrays.stream(clazz.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.toArray(Field[]::new);
for (Field field : fields) {
field.setAccessible(true);
}
PACKETS.put(clazz, fields);
}

if (VersionType.V1_8.isHigherOrEqual()) {
String enumSbActionClass = VersionType.V1_13.isHigherOrEqual()
? "ScoreboardServer$Action"
: "PacketPlayOutScoreboardScore$EnumScoreboardAction";
ENUM_SB_HEALTH_DISPLAY = FastReflection.nmsClass("IScoreboardCriteria$EnumScoreboardHealthDisplay");
ENUM_SB_ACTION = FastReflection.nmsClass(enumSbActionClass);
ENUM_SB_HEALTH_DISPLAY_INTEGER = FastReflection.enumValueOf(ENUM_SB_HEALTH_DISPLAY, "INTEGER");
ENUM_SB_ACTION_CHANGE = FastReflection.enumValueOf(ENUM_SB_ACTION, "CHANGE");
ENUM_SB_ACTION_REMOVE = FastReflection.enumValueOf(ENUM_SB_ACTION, "REMOVE");
ENUM_SB_HEALTH_DISPLAY = FastReflection.nmsClass("world.scores.criteria", "IScoreboardCriteria$EnumScoreboardHealthDisplay");
ENUM_SB_ACTION = FastReflection.nmsClass("server", enumSbActionClass);
ENUM_SB_HEALTH_DISPLAY_INTEGER = FastReflection.enumValueOf(ENUM_SB_HEALTH_DISPLAY, "INTEGER", 0);
ENUM_SB_ACTION_CHANGE = FastReflection.enumValueOf(ENUM_SB_ACTION, "CHANGE", 0);
ENUM_SB_ACTION_REMOVE = FastReflection.enumValueOf(ENUM_SB_ACTION, "REMOVE", 1);
} else {
ENUM_SB_HEALTH_DISPLAY = null;
ENUM_SB_ACTION = null;
ENUM_SB_HEALTH_DISPLAY_INTEGER = null;
ENUM_SB_ACTION_CHANGE = null;
ENUM_SB_ACTION_REMOVE = null;
}
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
} catch (Throwable t) {
throw new ExceptionInInitializerError(t);
}
}

Expand Down Expand Up @@ -520,10 +544,22 @@ private void sendTeamPacket(int score, TeamMode mode) throws Throwable {
suffix = (suffix != null) ? suffix.substring(0, maxLength) : null;
}

setComponentField(packet, prefix, 2); // Prefix
setComponentField(packet, suffix == null ? "" : suffix, 3); // Suffix
setField(packet, String.class, "always", 4); // Visibility for 1.8+
setField(packet, String.class, "always", 5); // Collisions for 1.9+
if (VersionType.V1_17.isHigherOrEqual()) {
Object team = PACKET_SB_SERIALIZABLE_TEAM.invoke();
// Since the packet is initialized with null values, we need to change more things.
setComponentField(team, "", 0); // Display name
setField(team, CHAT_FORMAT_ENUM, RESET_FORMATTING); // Color
setComponentField(team, prefix, 1); // Prefix
setComponentField(team, suffix == null ? "" : suffix, 2); // Suffix
setField(team, String.class, "always", 0); // Visibility
setField(team, String.class, "always", 1); // Collisions
setField(packet, Optional.class, Optional.of(team));
} else {
setComponentField(packet, prefix, 2); // Prefix
setComponentField(packet, suffix == null ? "" : suffix, 3); // Suffix
setField(packet, String.class, "always", 4); // Visibility for 1.8+
setField(packet, String.class, "always", 5); // Collisions for 1.9+
}

if (mode == TeamMode.CREATE) {
setField(packet, Collection.class, Collections.singletonList(getColorCode(score))); // Players in the team
Expand Down Expand Up @@ -559,15 +595,15 @@ private void setField(Object packet, Class<?> fieldType, Object value, int count
}

private void setComponentField(Object packet, String value, int count) throws Throwable {
if (VERSION_TYPE != VersionType.V1_13) {
if (!VersionType.V1_13.isHigherOrEqual()) {
setField(packet, String.class, value, count);
return;
}

int i = 0;
for (Field field : PACKETS.get(packet.getClass())) {
if ((field.getType() == String.class || field.getType() == CHAT_COMPONENT_CLASS) && count == i++) {
field.set(packet, Array.get(MESSAGE_FROM_STRING.invoke(value), 0));
field.set(packet, value.isEmpty() ? EMPTY_MESSAGE : Array.get(MESSAGE_FROM_STRING.invoke(value), 0));
}
}
}
Expand All @@ -585,7 +621,7 @@ enum ScoreboardAction {
}

enum VersionType {
V1_7, V1_8, V1_13;
V1_7, V1_8, V1_13, V1_17;

public boolean isHigherOrEqual() {
return VERSION_TYPE.ordinal() >= ordinal();
Expand Down
78 changes: 72 additions & 6 deletions src/main/java/fr/mrmicky/fastboard/FastReflection.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@
*/
package fr.mrmicky.fastboard;

import com.google.common.base.Suppliers;
import org.bukkit.Bukkit;
import sun.misc.Unsafe;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
* Small reflection utility class to use CraftBukkit and NMS.
Expand All @@ -34,25 +42,47 @@
*/
public final class FastReflection {

public static final String NM_PACKAGE = "net.minecraft";
public static final String OBC_PACKAGE = "org.bukkit.craftbukkit";
public static final String NMS_PACKAGE = "net.minecraft.server";
public static final String NMS_PACKAGE = NM_PACKAGE + ".server";

public static final String VERSION = Bukkit.getServer().getClass().getPackage().getName().substring(OBC_PACKAGE.length() + 1);

private static final MethodType VOID_METHOD_TYPE = MethodType.methodType(void.class);
private static final boolean NMS_REPACKAGED = optionalClass(NM_PACKAGE + ".network.protocol.Packet").isPresent();

private static final Supplier<Unsafe> UNSAFE = Suppliers.memoize(() -> {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
});

private FastReflection() {
throw new UnsupportedOperationException();
}

public static String nmsClassName(String className) {
public static boolean isRepackaged() {
return NMS_REPACKAGED;
}

public static String nmsClassName(String post1_17package, String className) {
if (NMS_REPACKAGED) {
String classPackage = post1_17package == null ? NM_PACKAGE : NM_PACKAGE + '.' + post1_17package;
return classPackage + '.' + className;
}
return NMS_PACKAGE + '.' + VERSION + '.' + className;
}

public static Class<?> nmsClass(String className) throws ClassNotFoundException {
return Class.forName(nmsClassName(className));
public static Class<?> nmsClass(String post1_17package, String className) throws ClassNotFoundException {
return Class.forName(nmsClassName(post1_17package, className));
}

public static Optional<Class<?>> nmsOptionalClass(String className) {
return optionalClass(nmsClassName(className));
public static Optional<Class<?>> nmsOptionalClass(String post1_17package, String className) {
return optionalClass(nmsClassName(post1_17package, className));
}

public static String obcClassName(String className) {
Expand All @@ -78,4 +108,40 @@ public static Optional<Class<?>> optionalClass(String className) {
public static Object enumValueOf(Class<?> enumClass, String enumName) {
return Enum.valueOf(enumClass.asSubclass(Enum.class), enumName);
}

public static Object enumValueOf(Class<?> enumClass, String enumName, int fallbackOrdinal) {
try {
return enumValueOf(enumClass, enumName);
} catch (IllegalArgumentException e) {
Object[] constants = enumClass.getEnumConstants();
if (constants.length > fallbackOrdinal) {
return constants[fallbackOrdinal];
}
throw e;
}
}

static Class<?> innerClass(Class<?> parentClass, Predicate<Class<?>> classPredicate) throws ClassNotFoundException {
for (Class<?> innerClass : parentClass.getDeclaredClasses()) {
if (classPredicate.test(innerClass)) {
return innerClass;
}
}
throw new ClassNotFoundException("No class in " + parentClass.getCanonicalName() + " matches the predicate.");
}

public static PacketConstructor findPacketConstructor(Class<?> packetClass, MethodHandles.Lookup lookup) {
try {
MethodHandle constructor = lookup.findConstructor(packetClass, VOID_METHOD_TYPE);
return constructor::invoke;
} catch (NoSuchMethodException | IllegalAccessException e) {
Unsafe unsafe = UNSAFE.get();
return () -> unsafe.allocateInstance(packetClass);
}
}

@FunctionalInterface
interface PacketConstructor {
Object invoke() throws Throwable;
}
}

0 comments on commit bf99e9f

Please sign in to comment.