diff --git a/README.md b/README.md index 98313585d..f9a13602c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ViaVersion 0.6.3 +# ViaVersion 0.6.7 [![Build Status](https://travis-ci.org/MylesIsCool/ViaVersion.svg?branch=master)](https://travis-ci.org/MylesIsCool/ViaVersion) **Allows the connection of 1.9 clients to 1.8** diff --git a/pom.xml b/pom.xml index 7aedb2374..2be236da3 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ us.myles viaversion - 0.6.4-SNAPSHOT + 0.6.8-SNAPSHOT jar ViaVersion @@ -101,6 +101,10 @@ org.spacehq.opennbt us.myles.viaversion.libs.opennbt + + com.google.gson + us.myles.viaversion.libs.gson + @@ -162,6 +166,15 @@ true + + + com.google.code.gson + gson + 2.6.2 + compile + true + + io.netty @@ -170,6 +183,7 @@ provided true + org.projectlombok diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java deleted file mode 100644 index 7ce493c28..000000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.spacehq.mc.protocol.data.game.chunk; - -import io.netty.buffer.ByteBuf; -import us.myles.ViaVersion.util.PacketUtil; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class BlockStorage { - private int bitsPerEntry; - - private List states; - private FlexibleStorage storage; - - public BlockStorage() { - this.bitsPerEntry = 4; - - this.states = new ArrayList<>(); - this.states.add(0); - - this.storage = new FlexibleStorage(this.bitsPerEntry, 4096); - } - - public BlockStorage(ByteBuf in) throws IOException { - this.bitsPerEntry = in.readUnsignedByte(); - - this.states = new ArrayList<>(); - int stateCount = PacketUtil.readVarInt(in); - for (int i = 0; i < stateCount; i++) { - this.states.add(PacketUtil.readVarInt(in)); - } - - this.storage = new FlexibleStorage(this.bitsPerEntry, PacketUtil.readLongs(PacketUtil.readVarInt(in), in)); - } - - private static int index(int x, int y, int z) { - return y << 8 | z << 4 | x; - } - - public void write(ByteBuf out) throws IOException { - out.writeByte(this.bitsPerEntry); - - PacketUtil.writeVarInt(this.states.size(), out); - for (int state : this.states) { - PacketUtil.writeVarInt(state, out); - } - - long[] data = this.storage.getData(); - PacketUtil.writeVarInt(data.length, out); - PacketUtil.writeLongs(data, out); - } - - public int getBitsPerEntry() { - return this.bitsPerEntry; - } - - public List getStates() { - return Collections.unmodifiableList(this.states); - } - - public FlexibleStorage getStorage() { - return this.storage; - } - - public int get(int x, int y, int z) { - int id = this.storage.get(index(x, y, z)); - return this.bitsPerEntry <= 8 ? (id >= 0 && id < this.states.size() ? this.states.get(id) : 0) : id; - } - - public void set(int x, int y, int z, int state) { - set(index(x, y, z), state); - } - - public void set(int ind, int state) { - int id = this.bitsPerEntry <= 8 ? this.states.indexOf(state) : state; - if (id == -1) { - this.states.add(state); - if (this.states.size() > 1 << this.bitsPerEntry) { - this.bitsPerEntry++; - - List oldStates = this.states; - if (this.bitsPerEntry > 8) { - oldStates = new ArrayList<>(this.states); - this.states.clear(); - this.bitsPerEntry = 13; - } - - FlexibleStorage oldStorage = this.storage; - this.storage = new FlexibleStorage(this.bitsPerEntry, this.storage.getSize()); - for (int index = 0; index < this.storage.getSize(); index++) { - int value = oldStorage.get(index); - this.storage.set(index, this.bitsPerEntry <= 8 ? value : oldStates.get(value)); - } - } - - id = this.bitsPerEntry <= 8 ? this.states.indexOf(state) : state; - } - - this.storage.set(ind, id); - } - - public boolean isEmpty() { - for (int index = 0; index < this.storage.getSize(); index++) { - if (this.storage.get(index) != 0) { - return false; - } - } - - return true; - } - - @Override - public boolean equals(Object o) { - return this == o || (o instanceof BlockStorage && this.bitsPerEntry == ((BlockStorage) o).bitsPerEntry && this.states.equals(((BlockStorage) o).states) && this.storage.equals(((BlockStorage) o).storage)); - } - - @Override - public int hashCode() { - int result = this.bitsPerEntry; - result = 31 * result + this.states.hashCode(); - result = 31 * result + this.storage.hashCode(); - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java deleted file mode 100644 index 6fb2af4b2..000000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.spacehq.mc.protocol.data.game.chunk; - -public class Chunk { - private BlockStorage blocks; - private NibbleArray3d blocklight; - private NibbleArray3d skylight; - - public Chunk(boolean skylight) { - this(new BlockStorage(), new NibbleArray3d(2048), skylight ? new NibbleArray3d(2048) : null); - } - - public Chunk(BlockStorage blocks, NibbleArray3d blocklight, NibbleArray3d skylight) { - this.blocks = blocks; - this.blocklight = blocklight; - this.skylight = skylight; - } - - public BlockStorage getBlocks() { - return this.blocks; - } - - public NibbleArray3d getBlockLight() { - return this.blocklight; - } - - public NibbleArray3d getSkyLight() { - return this.skylight; - } - - public boolean isEmpty() { - return this.blocks.isEmpty(); - } - - @Override - public boolean equals(Object o) { - return this == o || (o instanceof Chunk && this.blocks.equals(((Chunk) o).blocks) && this.blocklight.equals(((Chunk) o).blocklight) && ((this.skylight == null && (((Chunk) o).skylight == null)) || (this.skylight != null && this.skylight.equals(((Chunk) o).skylight)))); - } - - @Override - public int hashCode() { - int result = this.blocks.hashCode(); - result = 31 * result + this.blocklight.hashCode(); - result = 31 * result + (this.skylight != null ? this.skylight.hashCode() : 0); - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java deleted file mode 100644 index 6b84cbf43..000000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.spacehq.mc.protocol.data.game.chunk; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class Column { - private int x; - private int z; - private Chunk chunks[]; - private byte biomeData[]; - - private boolean skylight; - - public Column(int x, int z, Chunk chunks[]) { - this(x, z, chunks, null); - } - - public Column(int x, int z, Chunk chunks[], byte biomeData[]) { - if(chunks.length != 16) { - throw new IllegalArgumentException("Chunk array length must be 16."); - } - - if(biomeData != null && biomeData.length != 256) { - throw new IllegalArgumentException("Biome data array length must be 256."); - } - - this.skylight = false; - boolean noSkylight = false; - for (Chunk chunk : chunks) { - if (chunk != null) { - if (chunk.getSkyLight() == null) { - noSkylight = true; - } else { - this.skylight = true; - } - } - } - - if(noSkylight && this.skylight) { - throw new IllegalArgumentException("Either all chunks must have skylight values or none must have them."); - } - - this.x = x; - this.z = z; - this.chunks = chunks; - this.biomeData = biomeData; - } - - - public boolean hasBiomeData() { - return this.biomeData != null; - } - - public boolean hasSkylight() { - return this.skylight; - } -} \ No newline at end of file diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java deleted file mode 100644 index fcb75b4fc..000000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.spacehq.mc.protocol.data.game.chunk; - -import java.util.Arrays; - -public class FlexibleStorage { - private final long[] data; - private final int bitsPerEntry; - private final int size; - private final long maxEntryValue; - - public FlexibleStorage(int bitsPerEntry, int size) { - this(bitsPerEntry, new long[roundToNearest(size * bitsPerEntry, 64) / 64]); - } - - public FlexibleStorage(int bitsPerEntry, long[] data) { - if(bitsPerEntry < 1 || bitsPerEntry > 32) { - throw new IllegalArgumentException("BitsPerEntry cannot be outside of accepted range."); - } - - this.bitsPerEntry = bitsPerEntry; - this.data = data; - - this.size = this.data.length * 64 / this.bitsPerEntry; - this.maxEntryValue = (1L << this.bitsPerEntry) - 1; - } - - public long[] getData() { - return this.data; - } - - public int getBitsPerEntry() { - return this.bitsPerEntry; - } - - public int getSize() { - return this.size; - } - - public int get(int index) { - if(index < 0 || index > this.size - 1) { - throw new IndexOutOfBoundsException(); - } - - int bitIndex = index * this.bitsPerEntry; - int startIndex = bitIndex / 64; - int endIndex = ((index + 1) * this.bitsPerEntry - 1) / 64; - int startBitSubIndex = bitIndex % 64; - if(startIndex == endIndex) { - return (int) (this.data[startIndex] >>> startBitSubIndex & this.maxEntryValue); - } else { - int endBitSubIndex = 64 - startBitSubIndex; - return (int) ((this.data[startIndex] >>> startBitSubIndex | this.data[endIndex] << endBitSubIndex) & this.maxEntryValue); - } - } - - public void set(int index, int value) { - if(index < 0 || index > this.size - 1) { - throw new IndexOutOfBoundsException(); - } - - if(value < 0 || value > this.maxEntryValue) { - throw new IllegalArgumentException("Value cannot be outside of accepted range."); - } - - int bitIndex = index * this.bitsPerEntry; - int startIndex = bitIndex / 64; - int endIndex = ((index + 1) * this.bitsPerEntry - 1) / 64; - int startBitSubIndex = bitIndex % 64; - this.data[startIndex] = this.data[startIndex] & ~(this.maxEntryValue << startBitSubIndex) | ((long) value & this.maxEntryValue) << startBitSubIndex; - if(startIndex != endIndex) { - int endBitSubIndex = 64 - startBitSubIndex; - this.data[endIndex] = this.data[endIndex] >>> endBitSubIndex << endBitSubIndex | ((long) value & this.maxEntryValue) >> endBitSubIndex; - } - } - - private static int roundToNearest(int value, int roundTo) { - if(roundTo == 0) { - return 0; - } else if(value == 0) { - return roundTo; - } else { - if(value < 0) { - roundTo *= -1; - } - - int remainder = value % roundTo; - return remainder != 0 ? value + roundTo - remainder : value; - } - } - - @Override - public boolean equals(Object o) { - return this == o || (o instanceof FlexibleStorage && Arrays.equals(this.data, ((FlexibleStorage) o).data) && this.bitsPerEntry == ((FlexibleStorage) o).bitsPerEntry && this.size == ((FlexibleStorage) o).size && this.maxEntryValue == ((FlexibleStorage) o).maxEntryValue); - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(this.data); - result = 31 * result + this.bitsPerEntry; - result = 31 * result + this.size; - result = 31 * result + (int) this.maxEntryValue; - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java deleted file mode 100644 index a77fd10f8..000000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.spacehq.mc.protocol.data.game.chunk; - -import io.netty.buffer.ByteBuf; - -import java.io.IOException; -import java.util.Arrays; - -public class NibbleArray3d { - private byte[] data; - - public NibbleArray3d(int size) { - this.data = new byte[size]; - } - - public NibbleArray3d(byte[] array) { - this.data = array; - } - - public NibbleArray3d(ByteBuf in, int size) throws IOException { - this.data = new byte[size]; - in.readBytes(this.data); - } - - public void write(ByteBuf out) throws IOException { - out.writeBytes(this.data); - } - - public byte[] getData() { - return this.data; - } - - public int get(int x, int y, int z) { - int key = y << 8 | z << 4 | x; - int index = key >> 1; - int part = key & 1; - return part == 0 ? this.data[index] & 15 : this.data[index] >> 4 & 15; - } - - public void set(int x, int y, int z, int val) { - int key = y << 8 | z << 4 | x; - int index = key >> 1; - int part = key & 1; - if(part == 0) { - this.data[index] = (byte) (this.data[index] & 240 | val & 15); - } else { - this.data[index] = (byte) (this.data[index] & 15 | (val & 15) << 4); - } - } - - public void fill(int val) { - for(int index = 0; index < this.data.length << 1; index++) { - int ind = index >> 1; - int part = index & 1; - if(part == 0) { - this.data[ind] = (byte) (this.data[ind] & 240 | val & 15); - } else { - this.data[ind] = (byte) (this.data[ind] & 15 | (val & 15) << 4); - } - } - } - - @Override - public boolean equals(Object o) { - return this == o || (o instanceof NibbleArray3d && Arrays.equals(this.data, ((NibbleArray3d) o).data)); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.data); - } -} \ No newline at end of file diff --git a/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java b/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java deleted file mode 100644 index 623e12e49..000000000 --- a/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.spacehq.mc.protocol.util; - -import io.netty.buffer.ByteBuf; -import org.spacehq.mc.protocol.data.game.chunk.Chunk; -import org.spacehq.mc.protocol.data.game.chunk.Column; -import org.spacehq.mc.protocol.data.game.chunk.NibbleArray3d; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.ShortBuffer; - -/* From https://github.com/Steveice10/MCProtocolLib/ */ -/* No credit taken for writing this code, and used accordance to it's license - Original by Steveice10, modified to suit this plugin. - */ -public class NetUtil { - public static int writeNewColumn(ByteBuf out, Column column, boolean fullChunk, boolean hasSkylight) throws IOException { - int mask = 0; - Chunk chunks[] = column.getChunks(); - for (int index = 0; index < chunks.length; index++) { - Chunk chunk = chunks[index]; - if (chunk != null && (!fullChunk || !chunk.isEmpty())) { - mask |= 1 << index; - chunk.getBlocks().write(out); - chunk.getBlockLight().write(out); - if (hasSkylight || column.hasSkylight()) { - chunk.getSkyLight().write(out); // TODO: Make a PR to original lib to correct this - } - } - } - - if (fullChunk && column.getBiomeData() != null) { - out.writeBytes(column.getBiomeData()); - } - - return mask; - } - - public static Column readOldChunkData(int x, int z, boolean isFullChunk, int bitmask, byte[] input, boolean checkForSky, boolean hasSkyLight) { - int pos = 0; - int expected = isFullChunk ? 256 : 0; - boolean sky = false; - ShortBuffer buf = ByteBuffer.wrap(input).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); - // 0 = Calculate expected length and determine if the packet has skylight. - // 1 = Create chunks from mask and get blocks. - // 2 = Get block light. - // 3 = Get sky light. - Chunk[] chunks = new Chunk[16]; - int chunkCount = 0; - for (int pass = 0; pass < 4; pass++) { - if(pass == 1){ - if(chunkCount == 0) return null; - } - for (int ind = 0; ind < 16; ind++) { - if ((bitmask & 1 << ind) != 0) { - if (pass == 0) { - chunkCount++; - // Block length + Blocklight length - expected += (4096 * 2) + 2048; - } - - if (pass == 1) { - chunks[ind] = new Chunk(sky || hasSkyLight); - buf.position(pos / 2); - int buffPos = buf.position(); - // convert short array to new one - - for (int index = 0; index < 4096; index++) { - short ss = buf.get(buffPos + index); - // s is 16 bits, 12 bits id and 4 bits data - int data = ss & 0xF; - int id = (ss >> 4) << 4 | data; - - int newCombined = id; // test - - chunks[ind].getBlocks().set(index, newCombined); - } - pos += 4096 * 2; - - } - - if (pass == 2) { - NibbleArray3d blocklight = chunks[ind].getBlockLight(); - System.arraycopy(input, pos, blocklight.getData(), 0, blocklight.getData().length); - pos += blocklight.getData().length; - } - - if (pass == 3) { - if (sky) { - NibbleArray3d skylight = chunks[ind].getSkyLight(); - System.arraycopy(input, pos, skylight.getData(), 0, skylight.getData().length); - pos += skylight.getData().length; - } - } - } - } - - if (pass == 0) { - // If we have more data than blocks and blocklight combined, there must be skylight data as well. - if (input.length > expected) { - sky = checkForSky; - } - } - } - - byte biomeData[] = null; - if (isFullChunk && (pos + 256 <= input.length)) { - - biomeData = new byte[256]; - System.arraycopy(input, pos, biomeData, 0, biomeData.length); - } - - Column column = new Column(x, z, chunks, biomeData); - return column; - } -} 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/ViaVersionPlugin.java b/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java index df873c329..5cdbc3796 100644 --- a/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java +++ b/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java @@ -5,6 +5,7 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; +import lombok.NonNull; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -29,7 +30,6 @@ import us.myles.ViaVersion.util.ListWrapper; import us.myles.ViaVersion.util.ReflectionUtil; import java.io.File; -import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; @@ -39,7 +39,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { @@ -99,7 +98,7 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { public void generateConfig() { File file = new File(getDataFolder(), "config.yml"); - if(file.exists()) { + if (file.exists()) { // Update config options Configuration oldConfig = new Configuration(file); oldConfig.reload(false); // Load current options from config @@ -107,9 +106,9 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { saveDefaultConfig(); // Generate new config Configuration newConfig = new Configuration(file); newConfig.reload(true); // Load default options - for(String key : oldConfig.getKeys(false)) { + for (String key : oldConfig.getKeys(false)) { // Set option in new config if exists - if(newConfig.contains(key)) { + if (newConfig.contains(key)) { newConfig.set(key, oldConfig.get(key)); } } @@ -123,51 +122,45 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { try { Class serverClazz = ReflectionUtil.nms("MinecraftServer"); Object server = ReflectionUtil.invokeStatic(serverClazz, "getServer"); - Object connection = serverClazz.getDeclaredMethod("getServerConnection").invoke(server); - if (connection == null) { - System.out.println("connection is null!!"); - //try others - for (Method m : serverClazz.getDeclaredMethods()) { - if (m.getReturnType() != null && !m.getName().equals("getServerConnection")) { - if (m.getReturnType().getSimpleName().equals("ServerConnection")) { - if (m.getParameterTypes().length == 0) { - connection = m.invoke(server); - } + Object connection = null; + for (Method m : serverClazz.getDeclaredMethods()) { + if (m.getReturnType() != null) { + if (m.getReturnType().getSimpleName().equals("ServerConnection")) { + if (m.getParameterTypes().length == 0) { + connection = m.invoke(server); } } } - if (connection == null) { - getLogger().warning("We failed to find the ServerConnection? :("); - return; - } } - if (connection != null) { - for (Field field : connection.getClass().getDeclaredFields()) { - field.setAccessible(true); - final Object value = field.get(connection); - if (value instanceof List) { - // Inject the list - List wrapper = new ListWrapper((List) value) { - @Override - public synchronized void handleAdd(Object o) { - synchronized (this) { - if (o instanceof ChannelFuture) { - inject((ChannelFuture) o); - } - } - } - }; - field.set(connection, wrapper); - // Iterate through current list - synchronized (wrapper) { - for (Object o : (List) value) { + if (connection == null) { + getLogger().warning("We failed to find the ServerConnection? :( What server are you running?"); + return; + } + for (Field field : connection.getClass().getDeclaredFields()) { + field.setAccessible(true); + final Object value = field.get(connection); + if (value instanceof List) { + // Inject the list + List wrapper = new ListWrapper((List) value) { + @Override + public synchronized void handleAdd(Object o) { + synchronized (this) { if (o instanceof ChannelFuture) { inject((ChannelFuture) o); - } else { - break; // not the right list. } } } + }; + field.set(connection, wrapper); + // Iterate through current list + synchronized (wrapper) { + for (Object o : (List) value) { + if (o instanceof ChannelFuture) { + inject((ChannelFuture) o); + } else { + break; // not the right list. + } + } } } } @@ -201,6 +194,13 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { return isPorted(player.getUniqueId()); } + @Override + public int getPlayerVersion(@NonNull Player player) { + if (!isPorted(player)) + return 47; + return portedPlayers.get(player.getUniqueId()).getProtocol(); + } + @Override public boolean isPorted(UUID playerUUID) { return portedPlayers.containsKey(playerUUID); @@ -245,6 +245,10 @@ public class ViaVersionPlugin extends JavaPlugin implements ViaVersionAPI { return getConfig().getBoolean("prevent-collision", true); } + public boolean isNewEffectIndicator(){ + return getConfig().getBoolean("use-new-effect-indicator",true); + } + public boolean isSuppressMetadataErrors() { return getConfig().getBoolean("suppress-metadata-errors", false); } diff --git a/src/main/java/us/myles/ViaVersion/api/ViaVersionAPI.java b/src/main/java/us/myles/ViaVersion/api/ViaVersionAPI.java index bd0cbb24d..e05389461 100644 --- a/src/main/java/us/myles/ViaVersion/api/ViaVersionAPI.java +++ b/src/main/java/us/myles/ViaVersion/api/ViaVersionAPI.java @@ -10,13 +10,20 @@ import java.util.UUID; public interface ViaVersionAPI { /** - * Is player using 1.9? + * Is the player connection modified by ViaVersion? * - * @param player - * @return True if the client is on 1.9 + * @param player Bukkit player object + * @return True if the client is modified (At the moment it also means version 1.9 and higher) */ boolean isPorted(Player player); + /** + * Get protocol number from a player + * @param player Bukkit player object + * @return Protocol ID, For example (47=1.8-1.8.8, 107=1.9, 108=1.9.1) + */ + int getPlayerVersion(Player player); + /** * Is player using 1.9? * diff --git a/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java b/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java index 1e7001977..9c882de69 100644 --- a/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java +++ b/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java @@ -11,8 +11,10 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.inventory.CraftingInventory; import us.myles.ViaVersion.ViaVersionPlugin; import us.myles.ViaVersion.api.ViaVersion; @@ -89,6 +91,18 @@ public class ArmorListener implements Listener { sendDelayedArmorUpdate(e.getPlayer()); } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRespawn(PlayerRespawnEvent e) { + if (ViaVersion.getInstance().isPorted(e.getPlayer())) + sendDelayedArmorUpdate(e.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldChange(PlayerChangedWorldEvent e) { + if (ViaVersion.getInstance().isPorted(e.getPlayer())) + sendArmorUpdate(e.getPlayer()); + } + public void sendDelayedArmorUpdate(final Player player) { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override diff --git a/src/main/java/us/myles/ViaVersion/boss/ViaBossBar.java b/src/main/java/us/myles/ViaVersion/boss/ViaBossBar.java index e398cdee9..a9d80e08d 100644 --- a/src/main/java/us/myles/ViaVersion/boss/ViaBossBar.java +++ b/src/main/java/us/myles/ViaVersion/boss/ViaBossBar.java @@ -15,10 +15,7 @@ import us.myles.ViaVersion.packets.PacketType; import us.myles.ViaVersion.transformers.OutgoingTransformer; import us.myles.ViaVersion.util.PacketUtil; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.UUID; +import java.util.*; @Getter public class ViaBossBar implements BossBar { @@ -142,7 +139,7 @@ public class ViaBossBar implements BossBar { private void sendPacket(UpdateAction action) { ByteBuf buf = getPacket(action); - for (UUID uuid : players) + for (UUID uuid : new ArrayList<>(players)) sendPacket(uuid, buf); } diff --git a/src/main/java/us/myles/ViaVersion/chunks/Chunk.java b/src/main/java/us/myles/ViaVersion/chunks/Chunk.java new file mode 100644 index 000000000..8168cd1b1 --- /dev/null +++ b/src/main/java/us/myles/ViaVersion/chunks/Chunk.java @@ -0,0 +1,32 @@ +package us.myles.ViaVersion.chunks; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Chunk { + private final int x; + private final int z; + private final boolean groundUp; + private final int primaryBitmask; + private final ChunkSection[] sections; + private final byte[] biomeData; + private boolean unloadPacket = false; + + /** + * Chunk unload. + * + * @param x coord + * @param z coord + */ + protected Chunk(int x, int z) { + this(x, z, true, 0, new ChunkSection[16], null); + this.unloadPacket = true; + } + + public boolean hasBiomeData() { + return biomeData != null && groundUp; + } +} diff --git a/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java b/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java new file mode 100644 index 000000000..41c5dd955 --- /dev/null +++ b/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java @@ -0,0 +1,224 @@ +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; +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.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; + +public class ChunkManager { + /** + * Amount of sections in a chunk. + */ + private static final int SECTION_COUNT = 16; + /** + * size of each chunk section (16x16x16). + */ + private static final int SECTION_SIZE = 16; + /** + * Length of biome data. + */ + private static final int BIOME_DATA_LENGTH = 256; + + private final ConnectionInfo info; + private final Set loadedChunks = Sets.newConcurrentHashSet(); + 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 { + 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 transform chunk bulk", e); + } + return list; + } + + /** + * Read chunk from 1.8 chunk data. + * + * @param input data + * @return Chunk + */ + public Chunk readChunk(ByteBuf input) { + // Primary data + int chunkX = input.readInt(); + int chunkZ = input.readInt(); + long chunkHash = toLong(chunkX, chunkZ); + boolean groundUp = input.readByte() != 0; + int bitmask = input.readUnsignedShort(); + int dataLength = PacketUtil.readVarInt(input); + + // Data to be read + BitSet usedSections = new BitSet(16); + ChunkSection[] sections = new ChunkSection[16]; + byte[] biomeData = null; + + // Calculate section count from bitmask + for(int i = 0; i < 16; i++) { + if((bitmask & (1 << i)) != 0) { + usedSections.set(i); + } + } + int sectionCount = usedSections.cardinality(); // the amount of sections set + + // 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(); + loadedChunks.add(chunkHash); // mark chunk as loaded + + // Read blocks + for(int i = 0; i < SECTION_COUNT; i++) { + if(!usedSections.get(i)) continue; // Section not set + ChunkSection section = new ChunkSection(); + sections[i] = section; + + // Read block data and convert to short buffer + byte[] blockData = new byte[ChunkSection.SIZE * 2]; + input.readBytes(blockData); + ShortBuffer blockBuf = ByteBuffer.wrap(blockData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); + + for(int j = 0; j < ChunkSection.SIZE; j++) { + int mask = blockBuf.get(); + int type = mask >> 4; + int data = mask & 0xF; + section.setBlock(j, type, data); + } + } + + // Read block light + for(int i = 0; i < SECTION_COUNT; i++) { + if(!usedSections.get(i)) continue; // Section not set, has no light + byte[] blockLightArray = new byte[ChunkSection.LIGHT_LENGTH]; + input.readBytes(blockLightArray); + sections[i].setBlockLight(blockLightArray); + } + + // Read sky light + int bytesLeft = dataLength - (input.readerIndex() - startIndex); + if(bytesLeft >= ChunkSection.LIGHT_LENGTH) { + for(int i = 0; i < SECTION_COUNT; i++) { + if(!usedSections.get(i)) continue; // Section not set, has no light + byte[] skyLightArray = new byte[ChunkSection.LIGHT_LENGTH]; + input.readBytes(skyLightArray); + sections[i].setSkyLight(skyLightArray); + bytesLeft -= ChunkSection.LIGHT_LENGTH; + } + } + + // Read biome data + if(bytesLeft >= BIOME_DATA_LENGTH) { + biomeData = new byte[BIOME_DATA_LENGTH]; + input.readBytes(biomeData); + bytesLeft -= BIOME_DATA_LENGTH; + } + + // Check remaining bytes + if(bytesLeft > 0) { + Bukkit.getLogger().log(Level.WARNING, bytesLeft + " Bytes left after reading chunk! (" + groundUp + ")"); + } + + // Return chunk + return new Chunk(chunkX, chunkZ, groundUp, bitmask, sections, biomeData); + } + + /** + * Write chunk over 1.9 protocol. + * + * @param chunk chunk + * @param output output + */ + public void writeChunk(Chunk chunk, ByteBuf output) { + if(chunk.isUnloadPacket()) { + output.clear(); + PacketUtil.writeVarInt(0x1D, output); + } + + // Write primary info + output.writeInt(chunk.getX()); + output.writeInt(chunk.getZ()); + if(chunk.isUnloadPacket()) return; + output.writeByte(chunk.isGroundUp() ? 0x01 : 0x00); + PacketUtil.writeVarInt(chunk.getPrimaryBitmask(), output); + + ByteBuf buf = Unpooled.buffer(); + for(int i = 0; i < SECTION_COUNT; i++) { + ChunkSection section = chunk.getSections()[i]; + if(section == null) continue; // Section not set + section.writeBlocks(buf); + section.writeBlockLight(buf); + if(!section.hasSkyLight()) continue; // No sky light, we're done here. + section.writeSkyLight(buf); + } + buf.readerIndex(0); + PacketUtil.writeVarInt(buf.readableBytes() + (chunk.hasBiomeData() ? 256 : 0), output); + output.writeBytes(buf); + buf.release(); // release buffer + + // Write biome data + if(chunk.hasBiomeData()) { + output.writeBytes(chunk.getBiomeData()); + } + } + + private static long toLong(int msw, int lsw) { + return ((long) msw << 32) + lsw - -2147483648L; + } +} diff --git a/src/main/java/us/myles/ViaVersion/chunks/ChunkSection.java b/src/main/java/us/myles/ViaVersion/chunks/ChunkSection.java new file mode 100644 index 000000000..599da1ca8 --- /dev/null +++ b/src/main/java/us/myles/ViaVersion/chunks/ChunkSection.java @@ -0,0 +1,143 @@ +package us.myles.ViaVersion.chunks; + +import com.google.common.collect.Lists; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.bukkit.Material; +import us.myles.ViaVersion.util.PacketUtil; + +import java.util.List; + +public class ChunkSection { + /** + * Size (dimensions) of blocks in a chunk section. + */ + public static final int SIZE = 16 * 16 * 16; // width * depth * height + /** + * Length of the sky and block light nibble arrays. + */ + public static final int LIGHT_LENGTH = 16 * 16 * 16 / 2; // size * size * size / 2 (nibble bit count) + /** + * Length of the block data array. + */ +// public static final int BLOCK_LENGTH = 16 * 16 * 16 * 2; // size * size * size * 2 (char bit count) + + private final List palette = Lists.newArrayList(); + private final int[] blocks; + private final NibbleArray blockLight; + private NibbleArray skyLight; + + public ChunkSection() { + this.blocks = new int[SIZE]; + this.blockLight = new NibbleArray(SIZE); + palette.add(0); // AIR + } + + public void setBlock(int x, int y, int z, int type, int data) { + setBlock(index(x, y, z), type, data); + } + + public void setBlock(int idx, int type, int data) { + int hash = type << 4 | (data & 0xF); + int index = palette.indexOf(hash); + if(index == -1) { + index = palette.size(); + palette.add(hash); + } + + blocks[idx] = index; + } + + public void setBlockLight(byte[] data) { + blockLight.setHandle(data); + } + + public void setSkyLight(byte[] data) { + if(data.length != LIGHT_LENGTH) throw new IllegalArgumentException("Data length != " + LIGHT_LENGTH); + this.skyLight = new NibbleArray(data); + } + + private int index(int x, int y, int z) { + return z << 8 | y << 4 | x; + } + + public void writeBlocks(ByteBuf output) { + // Write bits per block + int bitsPerBlock = 4; + while(palette.size() > 1 << bitsPerBlock) { + bitsPerBlock += 1; + } + long maxEntryValue = (1L << bitsPerBlock) - 1; + output.writeByte(bitsPerBlock); + + // Write pallet (or not) + PacketUtil.writeVarInt(palette.size(), output); + for(int mappedId : palette) { + PacketUtil.writeVarInt(mappedId, output); + } + + int length = (int) Math.ceil(SIZE * bitsPerBlock / 64.0); + PacketUtil.writeVarInt(length, output); + long[] data = new long[length]; + for(int index = 0; index < blocks.length; index++) { + int value = blocks[index]; + int bitIndex = index * bitsPerBlock; + int startIndex = bitIndex / 64; + int endIndex = ((index + 1) * bitsPerBlock - 1) / 64; + int startBitSubIndex = bitIndex % 64; + data[startIndex] = data[startIndex] & ~(maxEntryValue << startBitSubIndex) | ((long) value & maxEntryValue) << startBitSubIndex; + if(startIndex != endIndex) { + int endBitSubIndex = 64 - startBitSubIndex; + data[endIndex] = data[endIndex] >>> endBitSubIndex << endBitSubIndex | ((long) value & maxEntryValue) >> endBitSubIndex; + } + } + PacketUtil.writeLongs(data, output); + } + + public void writeBlockLight(ByteBuf output) { + output.writeBytes(blockLight.getHandle()); + } + + public void writeSkyLight(ByteBuf output) { + output.writeBytes(skyLight.getHandle()); + } + + public boolean hasSkyLight() { + return skyLight != null; + } + + /** + * Get expected size of this chunk section. + * + * @return Amount of bytes sent by this section + */ + public int getExpectedSize() { + int bitsPerBlock = palette.size() > 255 ? 16 : 8; + int bytes = 1; // bits per block + bytes += paletteBytes(); // palette + bytes += countBytes(bitsPerBlock == 16 ? SIZE * 2 : SIZE); // block data length + bytes += (palette.size() > 255 ? 2 : 1) * SIZE; // block data + bytes += LIGHT_LENGTH; // block light + bytes += hasSkyLight() ? LIGHT_LENGTH : 0; // sky light + return bytes; + } + + private int paletteBytes() { + // Count bytes used by pallet + int bytes = countBytes(palette.size()); + for(int mappedId : palette) { + bytes += countBytes(mappedId); + } + return bytes; + } + + private int countBytes(int value) { + // Count amount of bytes that would be sent if the value were sent as a VarInt + ByteBuf buf = Unpooled.buffer(); + PacketUtil.writeVarInt(value, buf); + buf.readerIndex(0); + int bitCount = buf.readableBytes(); + buf.release(); + return bitCount; + } +} diff --git a/src/main/java/us/myles/ViaVersion/chunks/NibbleArray.java b/src/main/java/us/myles/ViaVersion/chunks/NibbleArray.java new file mode 100644 index 000000000..e8dd07565 --- /dev/null +++ b/src/main/java/us/myles/ViaVersion/chunks/NibbleArray.java @@ -0,0 +1,74 @@ +package us.myles.ViaVersion.chunks; + +import java.util.Arrays; + +public class NibbleArray { + private final byte[] handle; + + public NibbleArray(int length) { + if(length == 0 || length % 2 != 0) { + throw new IllegalArgumentException("Length of nibble array must be a positive number dividable by 2!"); + } + + this.handle = new byte[length / 2]; + } + + public NibbleArray(byte[] handle) { + if(handle.length == 0 || handle.length % 2 != 0) { + throw new IllegalArgumentException("Length of nibble array must be a positive number dividable by 2!"); + } + + this.handle = handle; + } + + public byte get(int x, int y, int z) { + return get(y << 8 | z << 4 | x); + } + + public byte get(int index) { + byte value = handle[index / 2]; + if(index % 2 == 0) { + return (byte) (value & 0xF); + } else { + return (byte) ((value >> 4) & 0xF); + } + } + + public void set(int x, int y, int z, int value) { + set(y << 8 | z << 4 | x, value); + } + + public void set(int index, int value) { + index /= 2; + if(index % 2 == 0) { + handle[index] = (byte) (handle[index] & 0xF0 | value & 0xF); + } else { + handle[index] = (byte) (handle[index] & 0xF | (value & 0xF) << 4); + } + } + + public int size() { + return handle.length * 2; + } + + public int actualSize() { + return handle.length; + } + + public void fill(byte value) { + value &= 0xF; // Max nibble size (= 16) + Arrays.fill(handle, (byte) ((value << 4) | value)); + } + + public void setHandle(byte[] handle) { + if(handle.length != this.handle.length) { + throw new IllegalArgumentException("Length of handle must equal to size of nibble array!"); + } + + System.arraycopy(handle, 0, this.handle, 0, handle.length); + } + + public byte[] getHandle() { + return handle; + } +} diff --git a/src/main/java/us/myles/ViaVersion/commands/ViaVersionCommand.java b/src/main/java/us/myles/ViaVersion/commands/ViaVersionCommand.java index 43b29f2a1..0605f548f 100644 --- a/src/main/java/us/myles/ViaVersion/commands/ViaVersionCommand.java +++ b/src/main/java/us/myles/ViaVersion/commands/ViaVersionCommand.java @@ -1,5 +1,6 @@ package us.myles.ViaVersion.commands; +import io.netty.util.ResourceLeakDetector; import lombok.RequiredArgsConstructor; import org.bukkit.Bukkit; import org.bukkit.command.Command; @@ -42,6 +43,13 @@ public class ViaVersionCommand implements CommandExecutor { } else if (args[0].equalsIgnoreCase("debug")) { plugin.setDebug(!plugin.isDebug()); sender.sendMessage(color("&6Debug mode is now " + (plugin.isDebug() ? "&aenabled" : "&cdisabled"))); + } else if (args[0].equalsIgnoreCase("displayleaks")) { + if (ResourceLeakDetector.getLevel() != ResourceLeakDetector.Level.ADVANCED) { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED); + } else { + ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED); + } + sender.sendMessage(color("&6Leak detector is now " + (ResourceLeakDetector.getLevel() == ResourceLeakDetector.Level.ADVANCED ? "&aenabled" : "&cdisabled"))); } else if (args[0].equalsIgnoreCase("dontbugme")) { boolean newValue = !plugin.getConfig().getBoolean("checkforupdates", true); plugin.getConfig().set("checkforupdates", newValue); @@ -62,7 +70,7 @@ public class ViaVersionCommand implements CommandExecutor { return false; } - public void sendHelp(CommandSender sender){ + public void sendHelp(CommandSender sender) { sender.sendMessage(color("&aViaVersion &c" + ViaVersion.getInstance().getVersion())); sender.sendMessage(color("&6Commands:")); sender.sendMessage(color("&2/viaversion list &7- &6Shows lists of all 1.9 clients and 1.8 clients.")); 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/handlers/ViaDecodeHandler.java b/src/main/java/us/myles/ViaVersion/handlers/ViaDecodeHandler.java index d283e39e1..652a2a337 100644 --- a/src/main/java/us/myles/ViaVersion/handlers/ViaDecodeHandler.java +++ b/src/main/java/us/myles/ViaVersion/handlers/ViaDecodeHandler.java @@ -8,6 +8,7 @@ import us.myles.ViaVersion.ConnectionInfo; import us.myles.ViaVersion.transformers.IncomingTransformer; import us.myles.ViaVersion.util.PacketUtil; +import java.lang.reflect.InvocationTargetException; import java.util.List; public class ViaDecodeHandler extends ByteToMessageDecoder { @@ -32,15 +33,27 @@ public class ViaDecodeHandler extends ByteToMessageDecoder { ByteBuf newPacket = ctx.alloc().buffer(); try { incomingTransformer.transform(id, bytebuf, newPacket); - bytebuf.clear(); bytebuf = newPacket; } catch (Exception e) { + // Clear Buffer bytebuf.clear(); + // Release Packet, be free! + newPacket.release(); throw e; } } // call minecraft decoder - list.addAll(PacketUtil.callDecode(this.minecraftDecoder, ctx, bytebuf)); + try { + list.addAll(PacketUtil.callDecode(this.minecraftDecoder, ctx, bytebuf)); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } + } finally { + if (info.isActive()) { + bytebuf.release(); + } + } } } diff --git a/src/main/java/us/myles/ViaVersion/handlers/ViaEncodeHandler.java b/src/main/java/us/myles/ViaVersion/handlers/ViaEncodeHandler.java index c50145bbd..41cfb7d81 100644 --- a/src/main/java/us/myles/ViaVersion/handlers/ViaEncodeHandler.java +++ b/src/main/java/us/myles/ViaVersion/handlers/ViaEncodeHandler.java @@ -7,9 +7,7 @@ import us.myles.ViaVersion.CancelException; import us.myles.ViaVersion.ConnectionInfo; import us.myles.ViaVersion.transformers.OutgoingTransformer; import us.myles.ViaVersion.util.PacketUtil; -import us.myles.ViaVersion.util.ReflectionUtil; -import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public class ViaEncodeHandler extends MessageToByteEncoder { @@ -29,7 +27,13 @@ public class ViaEncodeHandler extends MessageToByteEncoder { // handle the packet type if (!(o instanceof ByteBuf)) { // call minecraft encoder - PacketUtil.callEncode(this.minecraftEncoder, ctx, o, bytebuf); + try { + PacketUtil.callEncode(this.minecraftEncoder, ctx, o, bytebuf); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } + } } if (bytebuf.readableBytes() == 0) { throw new CancelException(); diff --git a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java index afb6878a7..8149248f2 100644 --- a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java +++ b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java @@ -1,8 +1,10 @@ package us.myles.ViaVersion.listeners; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import lombok.RequiredArgsConstructor; +import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.block.CommandBlock; import org.bukkit.entity.Player; @@ -10,14 +12,19 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.spacehq.opennbt.tag.builtin.ByteTag; +import org.spacehq.opennbt.tag.builtin.CompoundTag; import us.myles.ViaVersion.ViaVersionPlugin; import us.myles.ViaVersion.packets.PacketType; import us.myles.ViaVersion.util.PacketUtil; import us.myles.ViaVersion.util.ReflectionUtil; -import java.lang.reflect.InvocationTargetException; +import java.io.DataOutput; +import java.io.DataOutputStream; import java.lang.reflect.Method; @RequiredArgsConstructor @@ -26,14 +33,23 @@ public class CommandBlockListener implements Listener { private final ViaVersionPlugin plugin; @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) - public void onJoin(final PlayerJoinEvent e) { - if (e.getPlayer().isOp() && plugin.isPorted(e.getPlayer())) { - ByteBuf buf = Unpooled.buffer(); - PacketUtil.writeVarInt(PacketType.PLAY_ENTITY_STATUS.getNewPacketID(), buf); - buf.writeInt(e.getPlayer().getEntityId()); - buf.writeByte(26); - plugin.sendRawPacket(e.getPlayer(), buf); - } + public void onJoin(PlayerJoinEvent e) { + sendOp(e.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onRespawn(final PlayerRespawnEvent e) { + Bukkit.getScheduler().runTaskLater(plugin, new Runnable() { + @Override + public void run() { + sendOp(e.getPlayer()); + } + }, 1L); + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onWorldChange(PlayerChangedWorldEvent e) { + sendOp(e.getPlayer()); } @EventHandler(ignoreCancelled = true) @@ -41,22 +57,68 @@ public class CommandBlockListener implements Listener { if (e.getAction() == Action.RIGHT_CLICK_BLOCK && plugin.isPorted(e.getPlayer()) && e.getPlayer().isOp()) { try { sendCommandBlockPacket(e.getClickedBlock(), e.getPlayer()); - } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e1) { - e1.printStackTrace(); + } catch (Exception ex) { + ex.printStackTrace(); } } } - private void sendCommandBlockPacket(Block b, Player player) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException { + private void sendOp(Player p) { + if (p.isOp() && plugin.isPorted(p)) { + ByteBuf buf = Unpooled.buffer(); + PacketUtil.writeVarInt(PacketType.PLAY_ENTITY_STATUS.getNewPacketID(), buf); + buf.writeInt(p.getEntityId()); + buf.writeByte(26); + plugin.sendRawPacket(p, buf); + } + } + + private void sendCommandBlockPacket(Block b, Player player) throws Exception { if (!(b.getState() instanceof CommandBlock)) return; CommandBlock cmd = (CommandBlock) b.getState(); Object tileEntityCommand = ReflectionUtil.get(cmd, "commandBlock", ReflectionUtil.nms("TileEntityCommand")); Object updatePacket = ReflectionUtil.invoke(tileEntityCommand, "getUpdatePacket"); - Object nmsPlayer = ReflectionUtil.invoke(player, "getHandle"); - Object playerConnection = ReflectionUtil.get(nmsPlayer, "playerConnection", ReflectionUtil.nms("PlayerConnection")); - Method sendPacket = playerConnection.getClass().getMethod("sendPacket", ReflectionUtil.nms("Packet")); - sendPacket.invoke(playerConnection, updatePacket); //Let the transformer do the work + ByteBuf buf = packetToByteBuf(updatePacket); + plugin.sendRawPacket(player, buf); + } + + private ByteBuf packetToByteBuf(Object updatePacket) throws Exception { + ByteBuf buf = Unpooled.buffer(); + PacketUtil.writeVarInt(PacketType.PLAY_UPDATE_BLOCK_ENTITY.getNewPacketID(), buf); //Packet ID + long[] pos = getPosition(ReflectionUtil.get(updatePacket, "a", ReflectionUtil.nms("BlockPosition"))); + PacketUtil.writeBlockPosition(buf, pos[0], pos[1], pos[2]); //Block position + buf.writeByte(2); //Action id always 2 + CompoundTag nbt = getNBT(ReflectionUtil.get(updatePacket, "c", ReflectionUtil.nms("NBTTagCompound"))); + if (nbt == null) { + buf.writeByte(0); //If nbt is null. Use 0 as nbt + return buf; + } + nbt.put(new ByteTag("powered", (byte) 0)); + nbt.put(new ByteTag("auto", (byte) 0)); + nbt.put(new ByteTag("conditionMet", (byte) 0)); + PacketUtil.writeNBT(buf, nbt); //NBT tag + return buf; + } + + private long[] getPosition(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { + return new long[]{ + (long) ReflectionUtil.getSuper(obj, "a", int.class), //X + (long) ReflectionUtil.getSuper(obj, "c", int.class), //Y + (long) ReflectionUtil.getSuper(obj, "d", int.class) //Z + }; + } + + private CompoundTag getNBT(Object obj) throws Exception { + ByteBuf buf = Unpooled.buffer(); + Method m = ReflectionUtil.nms("NBTCompressedStreamTools").getMethod("a", ReflectionUtil.nms("NBTTagCompound"), DataOutput.class); + m.invoke(null, obj, new DataOutputStream(new ByteBufOutputStream(buf))); + try { + return PacketUtil.readNBT(buf); + } finally { + buf.release(); + } } } + diff --git a/src/main/java/us/myles/ViaVersion/metadata/MetaIndex.java b/src/main/java/us/myles/ViaVersion/metadata/MetaIndex.java index 06685b875..71af09183 100644 --- a/src/main/java/us/myles/ViaVersion/metadata/MetaIndex.java +++ b/src/main/java/us/myles/ViaVersion/metadata/MetaIndex.java @@ -34,6 +34,7 @@ public enum MetaIndex { PLAYER_ADDITIONAL_HEARTS(HumanEntity.class, 17, Type.Float, 10, NewType.Float), PLAYER_SCORE(HumanEntity.class, 18, Type.Int, 11, NewType.VarInt), PLAYER_HAND(HumanEntity.class, -1, Type.NonExistent, 5, NewType.Byte), // new in 1.9 + SOMETHING_ANTICHEAT_PLUGINS_FOR_SOME_REASON_USE(HumanEntity.class, 11, Type.Byte, NewType.Discontinued), //For what we know, This doesn't exists. If you think it exists and knows what it does. Please tell us. // horse HORSE_INFO(Horse.class, 16, Type.Int, 12, NewType.Byte), HORSE_TYPE(Horse.class, 19, Type.Byte, 13, NewType.VarInt), diff --git a/src/main/java/us/myles/ViaVersion/transformers/IncomingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/IncomingTransformer.java index e3b5c5699..75acf3441 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/IncomingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/IncomingTransformer.java @@ -45,7 +45,7 @@ public class IncomingTransformer { } // Handle movement increment // Update idle status (player, position, look, positionandlook) - if(packet == PacketType.PLAY_PLAYER || packet == PacketType.PLAY_PLAYER_POSITION_REQUEST || packet == PacketType.PLAY_PLAYER_LOOK_REQUEST || packet == PacketType.PLAY_PLAYER_POSITION_LOOK_REQUEST) { + if (packet == PacketType.PLAY_PLAYER || packet == PacketType.PLAY_PLAYER_POSITION_REQUEST || packet == PacketType.PLAY_PLAYER_LOOK_REQUEST || packet == PacketType.PLAY_PLAYER_POSITION_LOOK_REQUEST) { info.incrementIdlePacket(); } PacketUtil.writeVarInt(packetID, output); @@ -163,6 +163,18 @@ public class IncomingTransformer { if (packet == PacketType.PLAY_CLOSE_WINDOW_REQUEST) { info.closeWindow(); } + if (packet == PacketType.PLAY_CLIENT_STATUS) { + int action = PacketUtil.readVarInt(input); + PacketUtil.writeVarInt(action, input); + + if (action == 2) { + // cancel any blocking >.> + if (startedBlocking) { + sendSecondHandItem(null); + startedBlocking = false; + } + } + } if (packet == PacketType.PLAY_CLIENT_SETTINGS) { String locale = PacketUtil.readString(input); PacketUtil.writeString(locale, output); @@ -281,13 +293,13 @@ public class IncomingTransformer { } catch (Exception e) { e.printStackTrace(); } - short curX = input.readUnsignedByte(); - output.writeByte(curX); - short curY = input.readUnsignedByte(); - output.writeByte(curY); - short curZ = input.readUnsignedByte(); - output.writeByte(curZ); - return; + short curX = input.readUnsignedByte(); + output.writeByte(curX); + short curY = input.readUnsignedByte(); + output.writeByte(curY); + short curZ = input.readUnsignedByte(); + output.writeByte(curZ); + return; } if (packet == PacketType.PLAY_USE_ITEM) { int hand = PacketUtil.readVarInt(input); @@ -329,8 +341,17 @@ public class IncomingTransformer { } if (packet == PacketType.PLAY_CREATIVE_INVENTORY_ACTION) { short slot = input.readShort(); + if (slot == 45) { + ByteBuf buf = info.getChannel().alloc().buffer(); + PacketUtil.writeVarInt(PacketType.PLAY_SET_SLOT.getNewPacketID(), buf); + buf.writeByte(0); + buf.writeShort(slot); + buf.writeShort(-1); // empty + info.sendRawPacket(buf); + // Continue the packet simulating throw + slot = -999; + } output.writeShort(slot); - ItemSlotRewriter.rewrite1_9To1_8(input, output); } output.writeBytes(input); diff --git a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java index eed99fcf6..cee828de3 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java @@ -1,14 +1,11 @@ package us.myles.ViaVersion.transformers; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; import io.netty.buffer.ByteBuf; import org.bukkit.Material; import org.bukkit.entity.EntityType; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import org.spacehq.mc.protocol.data.game.chunk.Column; -import org.spacehq.mc.protocol.util.NetUtil; -import org.spacehq.opennbt.tag.builtin.ByteTag; import org.spacehq.opennbt.tag.builtin.CompoundTag; import org.spacehq.opennbt.tag.builtin.StringTag; import us.myles.ViaVersion.CancelException; @@ -18,6 +15,8 @@ import us.myles.ViaVersion.api.ViaVersion; import us.myles.ViaVersion.api.boss.BossBar; import us.myles.ViaVersion.api.boss.BossColor; import us.myles.ViaVersion.api.boss.BossStyle; +import us.myles.ViaVersion.chunks.Chunk; +import us.myles.ViaVersion.chunks.ChunkManager; import us.myles.ViaVersion.metadata.MetaIndex; import us.myles.ViaVersion.metadata.MetadataRewriter; import us.myles.ViaVersion.metadata.MetadataRewriter.Entry; @@ -27,7 +26,6 @@ import us.myles.ViaVersion.slot.ItemSlotRewriter; import us.myles.ViaVersion.sounds.SoundEffect; import us.myles.ViaVersion.util.EntityUtil; import us.myles.ViaVersion.util.PacketUtil; -import us.myles.ViaVersion.util.ReflectionUtil; import java.io.IOException; import java.util.*; @@ -36,6 +34,7 @@ import static us.myles.ViaVersion.util.PacketUtil.*; public class OutgoingTransformer { + private static Gson gson = new GsonBuilder().create(); private final ViaVersionPlugin plugin = (ViaVersionPlugin) ViaVersion.getInstance(); @@ -58,16 +57,16 @@ public class OutgoingTransformer { line = "{\"text\":\"\"}"; } else { if ((!line.startsWith("\"") || !line.endsWith("\"")) && (!line.startsWith("{") || !line.endsWith("}"))) { - JSONObject obj = new JSONObject(); - obj.put("text", line); - return obj.toJSONString(); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("text", line); + return gson.toJson(jsonObject); } if (line.startsWith("\"") && line.endsWith("\"")) { line = "{\"text\":" + line + "}"; } } try { - new JSONParser().parse(line); + gson.fromJson(line, JsonObject.class); } catch (Exception e) { System.out.println("Invalid JSON String: \"" + line + "\" Please report this issue to the ViaVersion Github: " + e.getMessage()); return "{\"text\":\"\"}"; @@ -299,11 +298,12 @@ public class OutgoingTransformer { if (packet == PacketType.STATUS_RESPONSE) { String originalStatus = PacketUtil.readString(input); try { - JSONObject json = (JSONObject) new JSONParser().parse(originalStatus); - JSONObject version = (JSONObject) json.get("version"); - version.put("protocol", info.getProtocol()); - PacketUtil.writeString(json.toJSONString(), output); - } catch (ParseException e) { + JsonObject jsonObject = gson.fromJson(originalStatus, JsonObject.class); + JsonObject version = jsonObject.get("version").getAsJsonObject(); + if (version.get("protocol").getAsInt() != 9999) //Fix ServerListPlus custom outdated message + version.addProperty("protocol", info.getProtocol()); + PacketUtil.writeString(gson.toJson(jsonObject), output); + } catch (Exception e) { e.printStackTrace(); } return; @@ -680,7 +680,7 @@ public class OutgoingTransformer { PacketUtil.writeVarInt(duration, output); // we need to write as a byte instead of boolean boolean hideParticles = input.readBoolean(); - output.writeByte(hideParticles ? 1 : 0); + output.writeByte(hideParticles ? plugin.isNewEffectIndicator() ? 2 : 1 : 0); return; } if (packet == PacketType.PLAY_TEAM) { @@ -757,73 +757,21 @@ public class OutgoingTransformer { return; } if (action == 2) { //Update commandblock - try { - CompoundTag nbt = readNBT(input); - if (nbt == null) - throw new CancelException(); - //Thanks http://www.minecraftforum.net/forums/minecraft-discussion/redstone-discussion-and/command-blocks/2488148-1-9-nbt-changes-and-additions#TileAllCommandBlocks - nbt.put(new ByteTag("powered", (byte) 0)); - nbt.put(new ByteTag("auto", (byte) 0)); - nbt.put(new ByteTag("conditionMet", (byte) 0)); - writeNBT(output, nbt); - return; - } catch (IOException e) { - e.printStackTrace(); - throw new CancelException(); - } + throw new CancelException(); //Only update if player interact with commandblock (The commandblock window will update every time this packet is sent, this would prevent you from change things that update every tick) } output.writeBytes(input, input.readableBytes()); return; } if (packet == PacketType.PLAY_CHUNK_DATA) { - // We need to catch unloading chunk packets as defined by wiki.vg - // To unload chunks, send this packet with Ground-Up Continuous=true and no 16^3 chunks (eg. Primary Bit Mask=0) - int chunkX = input.readInt(); - int chunkZ = input.readInt(); - output.writeInt(chunkX); - output.writeInt(chunkZ); - - - boolean groundUp = input.readBoolean(); - output.writeBoolean(groundUp); - - int bitMask = input.readUnsignedShort(); - int size = PacketUtil.readVarInt(input); - byte[] data = new byte[size]; - input.readBytes(data); -// if (bitMask == 0 && groundUp) { -// // if 256 -// output.clear(); -// PacketUtil.writeVarInt(PacketType.PLAY_UNLOAD_CHUNK.getNewPacketID(), output); -// output.writeInt(chunkX); -// output.writeInt(chunkZ); -// System.out.println("Sending unload chunk " + chunkX + " " + chunkZ + " - " + size + " bulk: " + bulk); -// return; -// } - boolean sk = false; - if (info.getLastPacket().getClass().getName().endsWith("PacketPlayOutMapChunkBulk")) { - try { - sk = ReflectionUtil.get(info.getLastPacket(), "d", boolean.class); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - } - Column read = NetUtil.readOldChunkData(chunkX, chunkZ, groundUp, bitMask, data, true, sk); - if (read == null) { + // Read chunk + ChunkManager chunkManager = info.getChunkManager(); + Chunk chunk = chunkManager.readChunk(input); + if (chunk == null) { throw new CancelException(); } - // Write chunk section array :(( - ByteBuf temp = output.alloc().buffer(); - try { - int bitmask = NetUtil.writeNewColumn(temp, read, groundUp, sk); - PacketUtil.writeVarInt(bitmask, output); - PacketUtil.writeVarInt(temp.readableBytes(), output); - output.writeBytes(temp); - } catch (IOException e) { - e.printStackTrace(); - } + + // Write chunk + chunkManager.writeChunk(chunk, output); return; } output.writeBytes(input); diff --git a/src/main/java/us/myles/ViaVersion/util/PacketUtil.java b/src/main/java/us/myles/ViaVersion/util/PacketUtil.java index a6922814e..f5942e724 100644 --- a/src/main/java/us/myles/ViaVersion/util/PacketUtil.java +++ b/src/main/java/us/myles/ViaVersion/util/PacketUtil.java @@ -42,6 +42,9 @@ public class PacketUtil { } public static CompoundTag readNBT(ByteBuf input) throws IOException { + // Default client is limited to 2097152 bytes. (2.09mb) + Preconditions.checkArgument(input.readableBytes() <= 2097152, "Cannot read NBT (got %s bytes)", input.readableBytes()); + int readerIndex = input.readerIndex(); byte b = input.readByte(); if (b == 0) { @@ -71,37 +74,24 @@ public class PacketUtil { } } - public static List callDecode(ByteToMessageDecoder decoder, ChannelHandlerContext ctx, Object input) { + public static List callDecode(ByteToMessageDecoder decoder, ChannelHandlerContext ctx, Object input) throws InvocationTargetException { List output = new ArrayList<>(); try { PacketUtil.DECODE_METHOD.invoke(decoder, ctx, input, output); - } catch (IllegalAccessException | InvocationTargetException e) { + } catch (IllegalAccessException e) { e.printStackTrace(); } return output; } - public static void callEncode(MessageToByteEncoder encoder, ChannelHandlerContext ctx, Object msg, ByteBuf output) { + public static void callEncode(MessageToByteEncoder encoder, ChannelHandlerContext ctx, Object msg, ByteBuf output) throws InvocationTargetException { try { PacketUtil.ENCODE_METHOD.invoke(encoder, ctx, msg, output); - } catch (IllegalAccessException | InvocationTargetException e) { + } catch (IllegalAccessException e) { e.printStackTrace(); } } - public static ByteBuf decompress(ChannelHandlerContext ctx, ByteBuf msg) { - ByteToMessageDecoder x = (ByteToMessageDecoder) ctx.pipeline().get("decompress"); - List output = callDecode(x, ctx, msg); - return output.size() == 0 ? null : (ByteBuf) output.get(0); - } - - public static ByteBuf compress(ChannelHandlerContext ctx, ByteBuf msg) { - MessageToByteEncoder x = (MessageToByteEncoder) ctx.pipeline().get("compress"); - ByteBuf output = ctx.alloc().buffer(); - callEncode(x, ctx, msg, output); - return output; - } - /* I take no credit, these are taken from BungeeCord */ // https://github.com/SpigotMC/BungeeCord/blob/master/protocol/src/main/java/net/md_5/bungee/protocol/DefinedPacket.java public static void writeString(String s, ByteBuf buf) { diff --git a/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java b/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java index 9b7829d3e..59c2c2096 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(); @@ -33,6 +37,12 @@ public class ReflectionUtil { return (T) field.get(null); } + public static T getSuper(Object o, String f, Class t) throws NoSuchFieldException, IllegalAccessException { + Field field = o.getClass().getSuperclass().getDeclaredField(f); + field.setAccessible(true); + return (T) field.get(o); + } + public static T get(Object instance, Class clazz, String f, Class t) throws NoSuchFieldException, IllegalAccessException { Field field = clazz.getDeclaredField(f); field.setAccessible(true); @@ -50,4 +60,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()); + } + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index d0287ba85..cdf3705d5 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -19,4 +19,6 @@ simulate-pt: true # Should we patch boss bars so they work? (Default: true, disable if you're having issues) bossbar-patch: true # If your boss bar flickers on 1.9, set this to 'true'. It will keep all boss bars on 100% (not recommended) -bossbar-anti-flicker: false \ No newline at end of file +bossbar-anti-flicker: false +# This will show the new effect indicator in the top-right corner for 1.9 players. +use-new-effect-indicator: true \ No newline at end of file