diff --git a/src/main/java/us/myles/ViaVersion/ConnectionInfo.java b/src/main/java/us/myles/ViaVersion/ConnectionInfo.java index 28bd84f85..b90f4a84d 100644 --- a/src/main/java/us/myles/ViaVersion/ConnectionInfo.java +++ b/src/main/java/us/myles/ViaVersion/ConnectionInfo.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.Setter; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import us.myles.ViaVersion.chunks.ChunkManager; import us.myles.ViaVersion.packets.State; @Getter @@ -16,6 +17,7 @@ public class ConnectionInfo { private static final long IDLE_PACKET_LIMIT = 20; // Max 20 ticks behind private final SocketChannel channel; + private final ChunkManager chunkManager; private Object lastPacket; private java.util.UUID UUID; private State state = State.HANDSHAKE; @@ -29,6 +31,7 @@ public class ConnectionInfo { public ConnectionInfo(SocketChannel socketChannel) { this.channel = socketChannel; + this.chunkManager = new ChunkManager(this); } public Player getPlayer() { diff --git a/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java b/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java index ee8a6faec..41c5dd955 100644 --- a/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java +++ b/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java @@ -1,5 +1,6 @@ package us.myles.ViaVersion.chunks; +import com.google.common.collect.Lists; import com.google.common.collect.Sets; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -7,13 +8,13 @@ import org.bukkit.Bukkit; import us.myles.ViaVersion.ConnectionInfo; import us.myles.ViaVersion.util.PacketUtil; import us.myles.ViaVersion.util.ReflectionUtil; +import us.myles.ViaVersion.util.ReflectionUtil.ClassReflection; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.ShortBuffer; import java.util.BitSet; +import java.util.List; import java.util.Set; import java.util.logging.Level; @@ -33,21 +34,54 @@ public class ChunkManager { private final ConnectionInfo info; private final Set loadedChunks = Sets.newConcurrentHashSet(); - private Method getWorldHandle; - private Method getChunkAt; - private Field getSections; + private final Set bulkChunks = Sets.newConcurrentHashSet(); + + // Reflection + private static ClassReflection mapChunkBulkRef; + private static ClassReflection mapChunkRef; + + static { + try { + mapChunkBulkRef = new ClassReflection(ReflectionUtil.nms("PacketPlayOutMapChunkBulk")); + mapChunkRef = new ClassReflection(ReflectionUtil.nms("PacketPlayOutMapChunk")); + } catch(Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Failed to initialise chunk reflection", e); + } + } public ChunkManager(ConnectionInfo info) { this.info = info; + } + /** + * Transform a map chunk bulk in to separate map chunk packets. + * These packets are registered so that they will never be seen as unload packets. + * + * @param packet to transform + * @return List of chunk data packets + */ + public List transformMapChunkBulk(Object packet) { + List list = Lists.newArrayList(); try { - this.getWorldHandle = ReflectionUtil.obc("CraftWorld").getDeclaredMethod("getHandle"); - this.getChunkAt = ReflectionUtil.nms("World").getDeclaredMethod("getChunkAt", int.class, int.class); - this.getSections = ReflectionUtil.nms("Chunk").getDeclaredField("sections"); - getSections.setAccessible(true); + int[] xcoords = mapChunkBulkRef.getFieldValue("a", packet, int[].class); + int[] zcoords = mapChunkBulkRef.getFieldValue("b", packet, int[].class); + Object[] chunkMaps = mapChunkBulkRef.getFieldValue("c", packet, Object[].class); + for(int i = 0; i < chunkMaps.length; i++) { + int x = xcoords[i]; + int z = zcoords[i]; + Object chunkMap = chunkMaps[i]; + Object chunkPacket = mapChunkRef.newInstance(); + mapChunkRef.setFieldValue("a", chunkPacket, x); + mapChunkRef.setFieldValue("b", chunkPacket, z); + mapChunkRef.setFieldValue("c", chunkPacket, chunkMap); + mapChunkRef.setFieldValue("d", chunkPacket, true); // Chunk bulk chunks are always ground-up + bulkChunks.add(toLong(x, z)); // Store for later + list.add(chunkPacket); + } } catch(Exception e) { - Bukkit.getLogger().log(Level.WARNING, "Failed to initialise chunk verification", e); + Bukkit.getLogger().log(Level.WARNING, "Failed to transform chunk bulk", e); } + return list; } /** @@ -76,35 +110,15 @@ public class ChunkManager { usedSections.set(i); } } - - // Unloading & empty chunks int sectionCount = usedSections.cardinality(); // the amount of sections set - if(sectionCount == 0 && groundUp) { - if(loadedChunks.contains(chunkHash)) { - // This is a chunk unload packet - loadedChunks.remove(chunkHash); - return new Chunk(chunkX, chunkZ); - } else { - // Check if chunk data is invalid - try { - Object nmsWorld = getWorldHandle.invoke(info.getPlayer().getWorld()); - Object nmsChunk = getChunkAt.invoke(info.getPlayer().getWorld()); - Object[] nmsSections = (Object[]) getSections.get(nmsChunk); - // Check if chunk is actually empty - boolean isEmpty = false; - int i = 0; - while(i < nmsSections.length) { - if(!(isEmpty = nmsSections[i++] == null)) break; - } - if(isEmpty) { - // not empty, LOL - return null; - } - } catch(Exception e) { - Bukkit.getLogger().log(Level.WARNING, "Failed to verify chunk", e); - } - } + // If the chunk is from a chunk bulk, it is never an unload packet + // Other wise, if it has no data, it is :) + boolean isBulkPacket = bulkChunks.remove(chunkHash); + if(sectionCount == 0 && groundUp && !isBulkPacket && loadedChunks.contains(chunkHash)) { + // This is a chunk unload packet + loadedChunks.remove(chunkHash); + return new Chunk(chunkX, chunkZ); } int startIndex = input.readerIndex(); diff --git a/src/main/java/us/myles/ViaVersion/handlers/ViaChunkHandler.java b/src/main/java/us/myles/ViaVersion/handlers/ViaChunkHandler.java index d3d73964d..27e85c045 100644 --- a/src/main/java/us/myles/ViaVersion/handlers/ViaChunkHandler.java +++ b/src/main/java/us/myles/ViaVersion/handlers/ViaChunkHandler.java @@ -26,24 +26,7 @@ public class ViaChunkHandler extends MessageToMessageEncoder { info.setLastPacket(o); /* This transformer is more for fixing issues which we find hard at packet level :) */ if(o.getClass().getName().endsWith("PacketPlayOutMapChunkBulk") && info.isActive()) { - final int[] locX = ReflectionUtil.get(o, "a", int[].class); - final int[] locZ = ReflectionUtil.get(o, "b", int[].class); - final Object world = ReflectionUtil.get(o, "world", ReflectionUtil.nms("World")); - Class mapChunk = ReflectionUtil.nms("PacketPlayOutMapChunk"); - final Constructor constructor = mapChunk.getDeclaredConstructor(ReflectionUtil.nms("Chunk"), boolean.class, int.class); - for(int i = 0; i < locX.length; i++) { - int x = locX[i]; - int z = locZ[i]; - // world invoke function - try { - Object chunk = ReflectionUtil.nms("World").getDeclaredMethod("getChunkAt", int.class, int.class).invoke(world, x, z); - Object packet = constructor.newInstance(chunk, true, 65535); - list.add(packet); - } catch(InstantiationException | InvocationTargetException | ClassNotFoundException | IllegalAccessException | NoSuchMethodException e) { - e.printStackTrace(); - } - } - + list.addAll(info.getChunkManager().transformMapChunkBulk(o)); return; } } diff --git a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java index be528ad53..26827c52f 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java @@ -42,7 +42,6 @@ public class OutgoingTransformer { private final ViaVersionPlugin plugin = (ViaVersionPlugin) ViaVersion.getInstance(); private final ConnectionInfo info; - private final ChunkManager chunkManager; private final Map uuidMap = new HashMap<>(); private final Map clientEntityTypes = new HashMap<>(); private final Map vehicleMap = new HashMap<>(); @@ -54,7 +53,6 @@ public class OutgoingTransformer { public OutgoingTransformer(ConnectionInfo info) { this.info = info; - this.chunkManager = new ChunkManager(info); } public static String fixJson(String line) { @@ -781,6 +779,7 @@ public class OutgoingTransformer { } if (packet == PacketType.PLAY_CHUNK_DATA) { // Read chunk + ChunkManager chunkManager = info.getChunkManager(); Chunk chunk = chunkManager.readChunk(input); if(chunk == null) { throw new CancelException(); diff --git a/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java b/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java index 9b7829d3e..ba9aa5005 100644 --- a/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java +++ b/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java @@ -1,10 +1,14 @@ package us.myles.ViaVersion.util; +import com.google.common.collect.Maps; import org.bukkit.Bukkit; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; public class ReflectionUtil { private static String BASE = Bukkit.getServer().getClass().getPackage().getName(); @@ -50,4 +54,74 @@ public class ReflectionUtil { field.setAccessible(true); field.set(o, value); } + + public static final class ClassReflection { + private final Class handle; + private final Map fields = Maps.newConcurrentMap(); + private final Map methods = Maps.newConcurrentMap(); + + public ClassReflection(Class handle) { + this(handle, true); + } + + public ClassReflection(Class handle, boolean recursive) { + this.handle = handle; + scanFields(handle, recursive); + scanMethods(handle, recursive); + } + + private void scanFields(Class host, boolean recursive) { + if(host.getSuperclass() != null && recursive) { + scanFields(host.getSuperclass(), true); + } + + for(Field field : host.getDeclaredFields()) { + field.setAccessible(true); + fields.put(field.getName(), field); + } + } + + private void scanMethods(Class host, boolean recursive) { + if(host.getSuperclass() != null && recursive) { + scanMethods(host.getSuperclass(), true); + } + + for(Method method : host.getDeclaredMethods()) { + method.setAccessible(true); + methods.put(method.getName(), method); + } + } + + public Object newInstance() throws IllegalAccessException, InstantiationException { + return handle.newInstance(); + } + + public Field getField(String name) { + return fields.get(name); + } + + public void setFieldValue(String fieldName, Object instance, Object value) throws IllegalAccessException { + getField(fieldName).set(instance, value); + } + + public T getFieldValue(String fieldName, Object instance, Class type) throws IllegalAccessException { + return type.cast(getField(fieldName).get(instance)); + } + + public T invokeMethod(Class type, String methodName, Object instance, Object... args) throws InvocationTargetException, IllegalAccessException { + return type.cast(getMethod(methodName).invoke(instance, args)); + } + + public Method getMethod(String name) { + return methods.get(name); + } + + public Collection getFields() { + return Collections.unmodifiableCollection(fields.values()); + } + + public Collection getMethods() { + return Collections.unmodifiableCollection(methods.values()); + } + } }