From 8b65efc4bd785d9a5a83b3479538d2aaef86cae5 Mon Sep 17 00:00:00 2001 From: Lennart ten Wolde <0p1q9o2w@hotmail.nl> Date: Sat, 12 Mar 2016 13:32:00 +0100 Subject: [PATCH 01/11] Implement new chunk transformer --- .../us/myles/ViaVersion/chunks/Chunk.java | 32 +++ .../myles/ViaVersion/chunks/ChunkManager.java | 210 ++++++++++++++++++ .../myles/ViaVersion/chunks/ChunkSection.java | 143 ++++++++++++ .../myles/ViaVersion/chunks/NibbleArray.java | 74 ++++++ .../transformers/OutgoingTransformer.java | 56 +---- 5 files changed, 469 insertions(+), 46 deletions(-) create mode 100644 src/main/java/us/myles/ViaVersion/chunks/Chunk.java create mode 100644 src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java create mode 100644 src/main/java/us/myles/ViaVersion/chunks/ChunkSection.java create mode 100644 src/main/java/us/myles/ViaVersion/chunks/NibbleArray.java 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..ee8a6faec --- /dev/null +++ b/src/main/java/us/myles/ViaVersion/chunks/ChunkManager.java @@ -0,0 +1,210 @@ +package us.myles.ViaVersion.chunks; + +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 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.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 Method getWorldHandle; + private Method getChunkAt; + private Field getSections; + + public ChunkManager(ConnectionInfo info) { + this.info = info; + + 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); + } catch(Exception e) { + Bukkit.getLogger().log(Level.WARNING, "Failed to initialise chunk verification", e); + } + } + + /** + * 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); + } + } + + // 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); + } + } + } + + 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/transformers/OutgoingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java index eed99fcf6..be528ad53 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java @@ -18,6 +18,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; @@ -40,6 +42,7 @@ 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<>(); @@ -51,6 +54,7 @@ public class OutgoingTransformer { public OutgoingTransformer(ConnectionInfo info) { this.info = info; + this.chunkManager = new ChunkManager(info); } public static String fixJson(String line) { @@ -776,54 +780,14 @@ public class OutgoingTransformer { 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 + 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); From e4369883038f1a48273b89656907693311b8ea5c Mon Sep 17 00:00:00 2001 From: Lennart ten Wolde <0p1q9o2w@hotmail.nl> Date: Sat, 12 Mar 2016 15:47:35 +0100 Subject: [PATCH 02/11] transform chunk bulk packets --- .../us/myles/ViaVersion/ConnectionInfo.java | 3 + .../myles/ViaVersion/chunks/ChunkManager.java | 88 +++++++++++-------- .../ViaVersion/handlers/ViaChunkHandler.java | 19 +--- .../transformers/OutgoingTransformer.java | 3 +- .../myles/ViaVersion/util/ReflectionUtil.java | 74 ++++++++++++++++ 5 files changed, 130 insertions(+), 57 deletions(-) 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()); + } + } } From 4f133366e16893e5be42ad025ef911c2ee674c6f Mon Sep 17 00:00:00 2001 From: Myles Date: Sat, 12 Mar 2016 20:41:04 +0000 Subject: [PATCH 03/11] Add limitation to NBT data, we'll stick with this for now. I don't see this easily being beaten. --- src/main/java/us/myles/ViaVersion/util/PacketUtil.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/us/myles/ViaVersion/util/PacketUtil.java b/src/main/java/us/myles/ViaVersion/util/PacketUtil.java index a6922814e..c5e118510 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) { From 44a044848fca6603948c06e765b17093b288a228 Mon Sep 17 00:00:00 2001 From: Myles Date: Sat, 12 Mar 2016 20:48:04 +0000 Subject: [PATCH 04/11] Fix ServerConnection #198 --- .../us/myles/ViaVersion/ViaVersionPlugin.java | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java b/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java index df873c329..3457228d7 100644 --- a/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java +++ b/src/main/java/us/myles/ViaVersion/ViaVersionPlugin.java @@ -29,7 +29,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 +38,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 +97,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 +105,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,23 +121,19 @@ 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) { + getLogger().warning("We failed to find the ServerConnection? :( What server are you running?"); + return; } if (connection != null) { for (Field field : connection.getClass().getDeclaredFields()) { From 8d2189cb8eccda372ecee14f2c6225a4672597b4 Mon Sep 17 00:00:00 2001 From: Myles Date: Sat, 12 Mar 2016 20:52:46 +0000 Subject: [PATCH 05/11] Bump versions --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98313585d..ce1d1f448 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ViaVersion 0.6.3 +# ViaVersion 0.6.4 [![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..9bd3cbf8f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ us.myles viaversion - 0.6.4-SNAPSHOT + 0.6.5-SNAPSHOT jar ViaVersion From 62d210de68d1976ca4e66d8ed5529a744941ac52 Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 11:06:18 +0100 Subject: [PATCH 06/11] Fix commandblocks not able to change command on every-tick commandblock update --- .../listeners/CommandBlockListener.java | 56 ++++++++++++++++--- .../transformers/OutgoingTransformer.java | 15 +---- .../myles/ViaVersion/util/ReflectionUtil.java | 6 ++ 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java index afb6878a7..aea3e95c3 100644 --- a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java +++ b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java @@ -1,6 +1,7 @@ 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.block.Block; @@ -12,12 +13,15 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerJoinEvent; +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 @@ -41,22 +45,58 @@ 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 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/transformers/OutgoingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java index 26827c52f..d1db0e8c8 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java @@ -759,20 +759,7 @@ 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; diff --git a/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java b/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java index ba9aa5005..59c2c2096 100644 --- a/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java +++ b/src/main/java/us/myles/ViaVersion/util/ReflectionUtil.java @@ -37,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); From 7d7f5a0bda022525c65c6e5fe2ee707af26d920f Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 14:02:10 +0100 Subject: [PATCH 07/11] Fix unable to open commandblock after respawn/world change --- .../listeners/CommandBlockListener.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java index aea3e95c3..8149248f2 100644 --- a/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java +++ b/src/main/java/us/myles/ViaVersion/listeners/CommandBlockListener.java @@ -4,6 +4,7 @@ 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; @@ -11,8 +12,10 @@ 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; @@ -30,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) @@ -51,6 +63,16 @@ public class CommandBlockListener implements Listener { } } + 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; From 53fb753284d4d1e570f135a2414698437b87a6aa Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 15:22:23 +0100 Subject: [PATCH 08/11] Fix armor not showing on Respawn (With keepinventory true) & world change --- .../us/myles/ViaVersion/armor/ArmorListener.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java b/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java index 1e7001977..d1b9be1dc 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,16 @@ public class ArmorListener implements Listener { sendDelayedArmorUpdate(e.getPlayer()); } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onRespawn(PlayerRespawnEvent e) { + sendDelayedArmorUpdate(e.getPlayer()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldChange(PlayerChangedWorldEvent e){ + sendArmorUpdate(e.getPlayer()); + } + public void sendDelayedArmorUpdate(final Player player) { Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { @Override From 7daa43146ef481a753e05c2a276528ab7d2259de Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 16:12:31 +0100 Subject: [PATCH 09/11] Drop spacehq MC protocol since it's no longer used --- .../data/game/chunk/BlockStorage.java | 126 ------------------ .../mc/protocol/data/game/chunk/Chunk.java | 46 ------- .../mc/protocol/data/game/chunk/Column.java | 59 -------- .../data/game/chunk/FlexibleStorage.java | 104 --------------- .../data/game/chunk/NibbleArray3d.java | 71 ---------- .../org/spacehq/mc/protocol/util/NetUtil.java | 117 ---------------- 6 files changed, 523 deletions(-) delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/util/NetUtil.java 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; - } -} From dd32b460554c0934fa9cd22f0b2c1b9a3f9b2197 Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 16:13:45 +0100 Subject: [PATCH 10/11] Remove unused imports --- .../myles/ViaVersion/transformers/OutgoingTransformer.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java index d1db0e8c8..3339d840d 100644 --- a/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java +++ b/src/main/java/us/myles/ViaVersion/transformers/OutgoingTransformer.java @@ -6,9 +6,6 @@ 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; @@ -29,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.*; @@ -768,7 +764,7 @@ public class OutgoingTransformer { // Read chunk ChunkManager chunkManager = info.getChunkManager(); Chunk chunk = chunkManager.readChunk(input); - if(chunk == null) { + if (chunk == null) { throw new CancelException(); } From 78ab7c840ad7647d38f7a372b3994fb5731fa7e1 Mon Sep 17 00:00:00 2001 From: Mats Date: Sun, 13 Mar 2016 16:48:07 +0100 Subject: [PATCH 11/11] Fix #209 woops my fault --- .../java/us/myles/ViaVersion/armor/ArmorListener.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java b/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java index d1b9be1dc..9c882de69 100644 --- a/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java +++ b/src/main/java/us/myles/ViaVersion/armor/ArmorListener.java @@ -93,12 +93,14 @@ public class ArmorListener implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onRespawn(PlayerRespawnEvent e) { - sendDelayedArmorUpdate(e.getPlayer()); + if (ViaVersion.getInstance().isPorted(e.getPlayer())) + sendDelayedArmorUpdate(e.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onWorldChange(PlayerChangedWorldEvent e){ - sendArmorUpdate(e.getPlayer()); + public void onWorldChange(PlayerChangedWorldEvent e) { + if (ViaVersion.getInstance().isPorted(e.getPlayer())) + sendArmorUpdate(e.getPlayer()); } public void sendDelayedArmorUpdate(final Player player) {