Rewrote reflection utilities to be failsafe, restructure project

This commit is contained in:
FlorianMichael 2024-04-02 13:06:33 +02:00
parent 74ad92d944
commit 0c82936698
No known key found for this signature in database
GPG Key ID: C2FB87E71C425126
13 changed files with 383 additions and 350 deletions

View File

@ -18,15 +18,15 @@
package com.viaversion.viarewind.legacysupport;
import com.viaversion.viarewind.legacysupport.injector.BoundingBoxFixer;
import com.viaversion.viarewind.legacysupport.feature.BoundingBoxFixer;
import com.viaversion.viarewind.legacysupport.versioninfo.VersionInformer;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viarewind.legacysupport.listener.AreaEffectCloudListener;
import com.viaversion.viarewind.legacysupport.listener.BounceListener;
import com.viaversion.viarewind.legacysupport.listener.BrewingListener;
import com.viaversion.viarewind.legacysupport.listener.ElytraListener;
import com.viaversion.viarewind.legacysupport.listener.EnchantingListener;
import com.viaversion.viarewind.legacysupport.listener.SoundListener;
import com.viaversion.viarewind.legacysupport.feature.AreaEffectCloudListener;
import com.viaversion.viarewind.legacysupport.feature.BounceListener;
import com.viaversion.viarewind.legacysupport.feature.BrewingListener;
import com.viaversion.viarewind.legacysupport.feature.ElytraListener;
import com.viaversion.viarewind.legacysupport.feature.EnchantingListener;
import com.viaversion.viarewind.legacysupport.feature.SoundListener;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
@ -35,6 +35,8 @@ import org.bukkit.scheduler.BukkitRunnable;
public class BukkitPlugin extends JavaPlugin {
private ProtocolVersion serverProtocol;
@Override
public void onEnable() {
// Make the config file
@ -49,7 +51,7 @@ public class BukkitPlugin extends JavaPlugin {
@Override
public void run() {
final ProtocolVersion serverProtocol = Via.getAPI().getServerVersion().lowestSupportedProtocolVersion();
serverProtocol = Via.getAPI().getServerVersion().lowestSupportedProtocolVersion();
if (!serverProtocol.isKnown()) return;
cancel();
@ -89,4 +91,13 @@ public class BukkitPlugin extends JavaPlugin {
}
}.runTaskTimer(this, 1L, 1L);
}
public ProtocolVersion getServerProtocol() {
return serverProtocol;
}
public static BukkitPlugin getInstance() {
return getPlugin(BukkitPlugin.class);
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viarewind.legacysupport.BukkitPlugin;
@ -24,7 +24,6 @@ import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.entity.AreaEffectCloud;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;

View File

@ -16,9 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.injector;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viarewind.legacysupport.reflection.ReflectionAPI;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import java.lang.reflect.Field;
@ -28,11 +27,14 @@ import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.viaversion.viarewind.legacysupport.util.ReflectionUtil.*;
import static com.viaversion.viarewind.legacysupport.util.NMSUtil.*;
public class BoundingBoxFixer {
public static void fixLilyPad(final Logger logger, final ProtocolVersion serverVersion) {
try {
final Field boundingBoxField = ReflectionAPI.getFieldAccessible(NMSReflection.getNMSBlock("BlockWaterLily"), serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_20_2) ? "a" : "b");
final Field boundingBoxField = getFieldAccessible(getNMSBlockClass("BlockWaterLily"), serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_20_2) ? "a" : "b");
setBoundingBox(boundingBoxField.get(null), 0.0625, 0.0, 0.0625, 0.9375, 0.015625, 0.9375);
} catch (Exception ex) {
@ -42,9 +44,9 @@ public class BoundingBoxFixer {
public static void fixCarpet(final Logger logger, final ProtocolVersion serverVersion) {
try {
final Class<?> blockCarpetClass = serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_16_4) ? NMSReflection.getNMSBlock("BlockCarpet") : NMSReflection.getNMSBlock("CarpetBlock");
final Class<?> blockCarpetClass = serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_16_4) ? getNMSBlockClass("BlockCarpet") : getNMSBlockClass("CarpetBlock");
final Field boundingBoxField = ReflectionAPI.getFieldAccessible(blockCarpetClass, serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_20_2) ? "a" : "b");
final Field boundingBoxField = getFieldAccessible(blockCarpetClass, serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_20_2) ? "a" : "b");
setBoundingBox(boundingBoxField.get(0), 0.0D, -0.0000001D, 0.0D, 1.0D, 0.0000001D, 1.0D);
} catch (Exception ex) {
logger.log(Level.SEVERE, "Could not fix carpet bounding box.", ex);
@ -58,12 +60,12 @@ public class BoundingBoxFixer {
final boolean pre1_16_4 = serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_16_4);
final boolean pre1_20_2 = serverVersion.olderThanOrEqualTo(ProtocolVersion.v1_20_2);
final Class<?> blockLadderClass = NMSReflection.getNMSBlock("BlockLadder");
final Class<?> blockLadderClass = getNMSBlockClass("BlockLadder");
final Field boundingBoxEastField = ReflectionAPI.getFieldAccessible(blockLadderClass, pre1_12_2 ? "b" : pre1_13_2 ? "c" : pre1_16_4 ? "c" : pre1_20_2 ? "d" : "e");
final Field boundingBoxWestField = ReflectionAPI.getFieldAccessible(blockLadderClass, pre1_12_2 ? "c" : pre1_13_2 ? "o" : pre1_16_4 ? "d" : pre1_20_2 ? "e" : "f");
final Field boundingBoxSouthField = ReflectionAPI.getFieldAccessible(blockLadderClass, pre1_12_2 ? "d" : pre1_13_2 ? "p" : pre1_16_4 ? "e" : pre1_20_2 ? "f" : "g");
final Field boundingBoxNorthField = ReflectionAPI.getFieldAccessible(blockLadderClass, pre1_12_2 ? "e" : pre1_13_2 ? "q" : pre1_16_4 ? "f" : pre1_20_2 ? "g" : "h");
final Field boundingBoxEastField = getFieldAccessible(blockLadderClass, pre1_12_2 ? "b" : pre1_13_2 ? "c" : pre1_16_4 ? "c" : pre1_20_2 ? "d" : "e");
final Field boundingBoxWestField = getFieldAccessible(blockLadderClass, pre1_12_2 ? "c" : pre1_13_2 ? "o" : pre1_16_4 ? "d" : pre1_20_2 ? "e" : "f");
final Field boundingBoxSouthField = getFieldAccessible(blockLadderClass, pre1_12_2 ? "d" : pre1_13_2 ? "p" : pre1_16_4 ? "e" : pre1_20_2 ? "f" : "g");
final Field boundingBoxNorthField = getFieldAccessible(blockLadderClass, pre1_12_2 ? "e" : pre1_13_2 ? "q" : pre1_16_4 ? "f" : pre1_20_2 ? "g" : "h");
setBoundingBox(boundingBoxEastField.get(null), 0.0D, 0.0D, 0.0D, 0.125D, 1.0D, 1.0D);
setBoundingBox(boundingBoxWestField.get(null), 0.875D, 0.0D, 0.0D, 1.0D, 1.0D, 1.0D);

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;

View File

@ -16,13 +16,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.listener;
package com.viaversion.viarewind.legacysupport.feature;
import com.viaversion.viarewind.legacysupport.reflection.MethodSignature;
import com.viaversion.viarewind.legacysupport.reflection.ReflectionAPI;
import com.viaversion.viaversion.api.Via;
import com.viaversion.viarewind.legacysupport.BukkitPlugin;
import com.viaversion.viarewind.legacysupport.injector.NMSReflection;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@ -42,6 +39,9 @@ import org.bukkit.event.player.PlayerPickupItemEvent;
import java.lang.reflect.Method;
import java.util.logging.Level;
import static com.viaversion.viarewind.legacysupport.util.ReflectionUtil.*;
import static com.viaversion.viarewind.legacysupport.util.NMSUtil.*;
@SuppressWarnings("unchecked")
public class SoundListener implements Listener {
@ -131,7 +131,7 @@ public class SoundListener implements Listener {
private static void playBlockPlaceSoundNMS(Player player, Block block) throws Exception {
World world = block.getWorld();
Object nmsWorld = world.getClass().getMethod("getHandle").invoke(world);
Class<?> blockPositionClass = NMSReflection.getBlockPositionClass();
Class<?> blockPositionClass = getBlockPositionClass();
Object blockPosition = null;
if (blockPositionClass != null)
@ -148,11 +148,11 @@ public class SoundListener implements Listener {
Method getStepSound;
final int serverProtocol = Via.getAPI().getServerVersion().lowestSupportedVersion();
if (serverProtocol > ProtocolVersion.v1_8.getVersion() && serverProtocol < ProtocolVersion.v1_12.getVersion()) {
getStepSound = ReflectionAPI.findRecursiveMethodOrNull(nmsBlock.getClass(), "w");
getStepSound = getMethod(nmsBlock.getClass(), "w");
} else if (serverProtocol > ProtocolVersion.v1_10.getVersion() && serverProtocol < ProtocolVersion.v1_13.getVersion()) {
getStepSound = ReflectionAPI.findRecursiveMethodOrNull(nmsBlock.getClass(), "getStepSound");
getStepSound = getMethod(nmsBlock.getClass(), "getStepSound");
} else { // 1.14 - 1.16.5
getStepSound = ReflectionAPI.findRecursiveMethodOrNull(nmsBlock.getClass(), "getStepSound", blockData.getClass());
getStepSound = getMethod(nmsBlock.getClass(), "getStepSound", blockData.getClass());
}
if (getStepSound == null) {
Via.getPlatform().getLogger().severe("Could not find getStepSound method in " + nmsBlock.getClass().getName());
@ -186,7 +186,7 @@ public class SoundListener implements Listener {
Object soundEffect = soundEffectMethod.invoke(soundType);
float volume = (float) volumeMethod.invoke(soundType);
float pitch = (float) pitchMethod.invoke(soundType);
Object soundCategory = Enum.valueOf(NMSReflection.getSoundCategoryClass(), "BLOCKS");
Object soundCategory = Enum.valueOf(getSoundCategoryClass(), "BLOCKS");
volume = (volume + 1.0f) / 2.0f;
pitch *= 0.8;
@ -197,7 +197,7 @@ public class SoundListener implements Listener {
// 1.8.8 -> 1.16.5
private static void playSound(Player player, Object soundEffect, Object soundCategory, double x, double y, double z, float volume, float pitch) {
try {
Object packet = NMSReflection.getGamePacketClass("PacketPlayOutNamedSoundEffect").getConstructor(
Object packet = getGamePacketClass("PacketPlayOutNamedSoundEffect").getConstructor(
soundEffect.getClass(), soundCategory.getClass(),
double.class, double.class, double.class,
float.class, float.class
@ -209,7 +209,7 @@ public class SoundListener implements Listener {
// Volume = 1
// Pitch = .8
NMSReflection.sendPacket(player, packet);
sendPacket(player, packet);
} catch (Exception ex) {
ex.printStackTrace();
}

View File

@ -1,141 +0,0 @@
/*
* This file is part of ViaRewind-Legacy-Support - https://github.com/ViaVersion/ViaRewind-Legacy-Support
* Copyright (C) 2018-2024 ViaVersion and contributors
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.injector;
import com.viaversion.viaversion.api.Via;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
public class NMSReflection {
private static final int PROTOCOL_1_17 = 755;
private static int protocolVersion = -1;
private static String version;
private static Field playerConnectionField;
public static String getVersion() {
return version == null ? version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3] : version;
}
public static int getProtocolVersion() {
return protocolVersion == -1 ? protocolVersion = Via.getAPI().getServerVersion().lowestSupportedVersion() : protocolVersion;
}
public static Class<?> getBlockPositionClass() {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.core.BlockPosition");
}
return getLegacyNMSClass("BlockPosition");
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
public static Class<?> getNMSBlock(String name) {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.world.level.block." + name);
}
return getLegacyNMSClass(name);
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
@SuppressWarnings("rawtypes")
public static Class getSoundCategoryClass() {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.sounds.SoundCategory");
}
return getLegacyNMSClass("SoundCategory");
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
public static Class<?> getPacketClass() {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.network.protocol.Packet");
}
return getLegacyNMSClass("Packet");
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
public static Class<?> getGamePacketClass(String packetType) {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.network.protocol.game." + packetType);
}
return getLegacyNMSClass(packetType);
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
public static Class<?> getPlayerConnectionClass() {
try {
if (getProtocolVersion() >= PROTOCOL_1_17) {
return Class.forName("net.minecraft.server.network.PlayerConnection");
}
return getLegacyNMSClass("PlayerConnection");
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return null;
}
public static Class<?> getLegacyNMSClass(String name) throws ClassNotFoundException {
return Class.forName("net.minecraft.server." + getVersion() + "." + name);
}
public static void sendPacket(Player player, Object packet) throws ReflectiveOperationException {
Object nmsPlayer = player.getClass().getMethod("getHandle").invoke(player);
if (playerConnectionField == null) {
playerConnectionField = Arrays.stream(nmsPlayer.getClass().getFields())
.filter(field -> field.getType() == getPlayerConnectionClass()).findFirst()
.orElseThrow(() -> new ReflectiveOperationException("Failed to find PlayerConnection field in EntityPlayer"));
}
Object playerConnection = playerConnectionField.get(nmsPlayer);
Method sendPacket;
try { // TODO find better way
sendPacket = playerConnection.getClass().getDeclaredMethod("sendPacket", getPacketClass());
} catch (Exception e) {
try {
sendPacket = playerConnection.getClass().getDeclaredMethod("a", getPacketClass());
} catch (Exception e2) {
throw new ReflectiveOperationException("Failed to find sendPacket method in PlayerConnection");
}
}
sendPacket.invoke(playerConnection, packet);
}
}

View File

@ -1,61 +0,0 @@
/*
* This file is part of ViaRewind-Legacy-Support - https://github.com/ViaVersion/ViaRewind-Legacy-Support
* Copyright (C) 2018-2024 ViaVersion and contributors
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.reflection;
import java.util.Arrays;
import java.util.Objects;
import java.util.StringJoiner;
public class MethodSignature {
private final String name;
private final Class<?>[] parameterTypes;
private Class<?> returnType;
public MethodSignature(String name, Class<?>... parameterTypes) {
this.name = name;
this.parameterTypes = parameterTypes;
}
public String name() {
return name;
}
public Class<?>[] parameterTypes() {
return parameterTypes;
}
public Class<?> returnType() {
return returnType;
}
public MethodSignature withReturnType(Class<?> returnType) {
Objects.requireNonNull(returnType);
this.returnType = returnType;
return this;
}
@Override
public String toString() {
return new StringJoiner(", ", MethodSignature.class.getSimpleName() + "[", "]")
.add("name='" + name + "'")
.add("parameterTypes=" + Arrays.toString(parameterTypes))
.toString();
}
}

View File

@ -1,113 +0,0 @@
/*
* This file is part of ViaRewind-Legacy-Support - https://github.com/ViaVersion/ViaRewind-Legacy-Support
* Copyright (C) 2018-2024 ViaVersion and contributors
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.reflection;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ReflectionAPI {
private static final Map<String, Field> fields = new HashMap<>();
private static boolean staticFinalModificationBlocked;
static {
try {
Field.class.getDeclaredField("modifiers");
} catch (NoSuchFieldException ex) {
staticFinalModificationBlocked = true;
}
}
public static Field getField(Class clazz, String fieldname) {
String key = clazz.getName() + ":" + fieldname;
Field field = null;
if (fields.containsKey(key)) {
field = fields.get(key);
} else {
try {
field = clazz.getDeclaredField(fieldname);
} catch (NoSuchFieldException ignored) {
}
fields.put(key, field);
}
return field;
}
public static Field getFieldAccessible(Class clazz, String fieldname) {
Field field = getField(clazz, fieldname);
if (field != null) field.setAccessible(true);
return field;
}
public static void setFieldNotFinal(Field field) {
int modifiers = field.getModifiers();
if (!Modifier.isFinal(modifiers)) return;
if (staticFinalModificationBlocked) {
try {
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
for (Field classField : fields) {
if ("modifiers".equals(classField.getName())) {
classField.setAccessible(true);
classField.set(field, modifiers & ~Modifier.FINAL);
break;
}
}
} catch (ReflectiveOperationException ex) {
ex.printStackTrace();
}
} else {
setValuePrintException(Field.class, field, "modifiers", modifiers & ~Modifier.FINAL);
}
}
public static void setValue(Class clazz, Object object, String fieldname, Object value, boolean isFinal) throws IllegalAccessException {
Field field = getFieldAccessible(clazz, fieldname);
if (isFinal) setFieldNotFinal(field);
field.set(object, value);
}
public static void setValue(Class clazz, Object object, String fieldname, Object value) throws IllegalAccessException {
setValue(clazz, object, fieldname, value, false);
}
public static void setValuePrintException(Class clazz, Object object, String fieldname, Object value) {
try {
setValue(clazz, object, fieldname, value);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static Method findRecursiveMethodOrNull(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ex) {
Class<?> superClass = clazz.getSuperclass();
if (superClass == null) return null;
return findRecursiveMethodOrNull(superClass, methodName, parameterTypes);
}
}
}

View File

@ -0,0 +1,139 @@
/*
* This file is part of ViaRewind-Legacy-Support - https://github.com/ViaVersion/ViaRewind-Legacy-Support
* Copyright (C) 2018-2024 ViaVersion and contributors
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.util;
import com.viaversion.viarewind.legacysupport.BukkitPlugin;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import static com.viaversion.viarewind.legacysupport.util.ReflectionUtil.failSafeGetClass;
public class NMSUtil {
public static String nmsVersionPackage;
private static Field playerConnectionField;
static {
nmsVersionPackage = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
}
public static Class<?> getBlockPositionClass() {
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.core.BlockPosition");
} else {
return getLegacyNMSClass("BlockPosition");
}
}
public static Class<?> getNMSBlockClass(final String name) {
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.world.level.block." + name);
} else {
return getLegacyNMSClass(name);
}
}
public static Class getSoundCategoryClass() { // Bypass generics
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.sounds.SoundCategory");
} else {
return getLegacyNMSClass("SoundCategory");
}
}
public static Class<?> getPacketClass() {
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.network.protocol.Packet");
} else {
return getLegacyNMSClass("Packet");
}
}
public static Class<?> getGamePacketClass(final String packet) {
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.network.protocol.game." + packet);
} else {
return getLegacyNMSClass(packet);
}
}
public static Class<?> getPlayerConnectionClass() {
if (BukkitPlugin.getInstance().getServerProtocol().newerThanOrEqualTo(ProtocolVersion.v1_17)) {
return failSafeGetClass("net.minecraft.server.network.PlayerConnection");
} else {
return getLegacyNMSClass("PlayerConnection");
}
}
public static void sendPacket(final Player player, final Object packet) {
Object nmsPlayer = null;
try {
player.getClass().getMethod("getHandle").invoke(player);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to get EntityPlayer from player", e);
return;
}
// Cache result as it never changes
if (playerConnectionField == null) {
final Class<?> playerConnection = getPlayerConnectionClass();
for (Field field : nmsPlayer.getClass().getFields()) {
if (field.getType() == playerConnection) {
playerConnectionField = field;
break;
}
}
// If reflection failed, log and return
if (playerConnectionField == null) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to find PlayerConnection field in EntityPlayer");
return;
}
}
Object playerConnection;
try {
playerConnection = playerConnectionField.get(nmsPlayer);
} catch (IllegalAccessException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to get PlayerConnection from EntityPlayer", e);
return;
}
try {
final Method sendPacket = ReflectionUtil.findMethod(player.getClass(), new String[] {"sendPacket", "a"}, getPacketClass());
sendPacket.invoke(playerConnection, packet);
} catch (IllegalAccessException | InvocationTargetException | NullPointerException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to send packet to player", e);
}
}
public static Class<?> getLegacyNMSClass(final String name) {
try {
return Class.forName("net.minecraft.server." + nmsVersionPackage + "." + name);
} catch (ClassNotFoundException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Could not find NMS class " + name + "! NMS version package: " + nmsVersionPackage, e);
return null;
}
}
}

View File

@ -0,0 +1,197 @@
/*
* This file is part of ViaRewind-Legacy-Support - https://github.com/ViaVersion/ViaRewind-Legacy-Support
* Copyright (C) 2018-2024 ViaVersion and contributors
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package com.viaversion.viarewind.legacysupport.util;
import com.viaversion.viarewind.legacysupport.BukkitPlugin;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* Common reflection utilities for Java 8 and newer.
*/
public class ReflectionUtil {
private static final Map<String, Field> fieldCache = new HashMap<>();
private static boolean staticFinalModificationBlocked;
static {
try {
Field.class.getDeclaredField("modifiers");
} catch (NoSuchFieldException ex) {
staticFinalModificationBlocked = true;
}
}
public static Method findMethod(final Class<?> clazz, final String[] methodNames, final Class<?>... parameterTypes) {
for (String methodName : methodNames) {
final Method method = getMethod(clazz, methodName, parameterTypes);
if (method != null) {
return method;
}
}
return null;
}
/**
* Recursively search for a method in the class and its superclasses. Returns null if the method is not found.
*
* @param clazz The class to search in
* @param methodName The name of the method
* @param parameterTypes The parameter types of the method
* @return The method if found, otherwise null
*/
public static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(methodName, parameterTypes);
} catch (NoSuchMethodException ex) {
final Class<?> superClass = clazz.getSuperclass();
if (superClass == null) return null;
return getMethod(superClass, methodName, parameterTypes);
}
}
public static Field getFieldAndCache(final Class<?> clazz, final String fieldName) {
final String key = clazz.getName() + ":" + fieldName;
if (fieldCache.containsKey(key)) {
return fieldCache.get(key);
} else {
final Field field = failSafeGetField(clazz, fieldName);
fieldCache.put(key, field);
return field;
}
}
/**
* Gets and field from the {@link #fieldCache} or directly and makes it accessible.
*
* @param clazz The class
* @param fieldName The field name
* @return The field
*/
public static Field getFieldAccessible(final Class<?> clazz, final String fieldName) {
final Field field = getFieldAndCache(clazz, fieldName);
if (field != null) {
field.setAccessible(true);
}
return field;
}
/**
* Get a field from a class, and print an error message if it fails.
*
* @param clazz The class
* @param fieldName The field name
* @return The field, or null if it fails
*/
public static Field failSafeGetField(final Class<?> clazz, final String fieldName) {
try {
return getFieldAccessible(clazz, fieldName);
} catch (Exception e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to get field " + fieldName + " in class " + clazz.getName(), e);
return null;
}
}
public static void setValue(final Class<?> clazz, final Object object, final String name, final Object value, final boolean isFinal) throws IllegalAccessException {
final Field field = getFieldAccessible(clazz, name);
if (isFinal) {
removeFinal(field);
}
field.set(object, value);
}
/**
* Remove the final modifier from a field.
*
* @param field The field
*/
public static void removeFinal(final Field field) {
final int modifiers = field.getModifiers();
if (!Modifier.isFinal(modifiers)) {
// Non-finals don't need to be modified
return;
}
if (!staticFinalModificationBlocked) {
// Older Java versions allow us to modify the modifiers directly and remove
// the final modifier
failSafeSetValue(Field.class, field, "modifiers", modifiers & ~Modifier.FINAL);
return;
}
// Modern Java bypass
try {
final Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
final Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
for (Field classField : fields) {
if ("modifiers".equals(classField.getName())) {
classField.setAccessible(true);
classField.set(field, modifiers & ~Modifier.FINAL);
break;
}
}
} catch (ReflectiveOperationException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to remove final modifier from field " + field.getName(), e);
}
}
/**
* Unsafe wrapper for value setting which bypasses final checks.
*
* @param clazz The class
* @param object The object
* @param name The field name
* @param value The value
* @throws IllegalAccessException If the field cannot be accessed
*/
public static void setValue(final Class<?> clazz, final Object object, final String name, final Object value) throws IllegalAccessException {
setValue(clazz, object, name, value, false);
}
/**
* Set a field value, and print an error message if it fails.
*
* @param clazz The class
* @param object The object
* @param name The field name
* @param value The value
*/
public static void failSafeSetValue(final Class<?> clazz, final Object object, final String name, final Object value) {
try {
setValue(clazz, object, name, value);
} catch (Exception e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to set value for field " + name + " in class " + clazz.getName(), e);
}
}
public static Class<?> failSafeGetClass(final String name) {
try {
return Class.forName(name);
} catch (ClassNotFoundException e) {
BukkitPlugin.getInstance().getLogger().log(Level.SEVERE, "Failed to get class " + name, e);
return null;
}
}
}