diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..52d913562
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: astei
diff --git a/api/build.gradle b/api/build.gradle
index a7e2c2298..db1147901 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -76,13 +76,13 @@ javadoc {
options.encoding = 'UTF-8'
options.charSet = 'UTF-8'
options.source = '8'
-// options.links(
-// 'https://www.slf4j.org/apidocs/',
-// 'https://guava.dev/releases/30.0-jre/api/docs/',
-// 'https://google.github.io/guice/api-docs/4.2/javadoc/',
-// 'https://docs.oracle.com/javase/8/docs/api/',
-// 'https://jd.adventure.kyori.net/api/4.0.0/'
-// )
+ options.links(
+ 'http://www.slf4j.org/apidocs/',
+ 'https://google.github.io/guava/releases/30.0-jre/api/docs/',
+ 'https://google.github.io/guice/api-docs/4.2/javadoc/',
+ 'https://docs.oracle.com/javase/8/docs/api/',
+ 'https://jd.adventure.kyori.net/api/4.7.0/'
+ )
// Disable the crazy super-strict doclint tool in Java 8
options.addStringOption('Xdoclint:none', '-quiet')
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java
index b7586c781..9aa7daf70 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/connection/Player.java
@@ -8,7 +8,8 @@
package com.velocitypowered.api.proxy.connection;
import com.velocitypowered.api.command.CommandSource;
-import com.velocitypowered.api.event.player.PlayerResourcePackStatusEventImpl;
+import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
+import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.player.ClientSettings;
@@ -123,7 +124,7 @@ public interface Player extends CommandSource, Identified, InboundConnection,
/**
* Sends the specified resource pack from {@code url} to the user. If at all possible, send the
* resource pack using {@link #sendResourcePack(String, byte[])}. To monitor the status of the
- * sent resource pack, subscribe to {@link PlayerResourcePackStatusEventImpl}.
+ * sent resource pack, subscribe to {@link PlayerResourcePackStatusEvent}.
*
* @param url the URL for the resource pack
*/
@@ -132,10 +133,22 @@ public interface Player extends CommandSource, Identified, InboundConnection,
/**
* Sends the specified resource pack from {@code url} to the user, using the specified 20-byte
* SHA-1 hash. To monitor the status of the sent resource pack, subscribe to
- * {@link PlayerResourcePackStatusEventImpl}.
+ * {@link PlayerResourcePackStatusEvent}.
*
* @param url the URL for the resource pack
* @param hash the SHA-1 hash value for the resource pack
*/
void sendResourcePack(String url, byte[] hash);
+
+ /**
+ * Note that this method does not send a plugin message to the server the player
+ * is connected to. You should only use this method if you are trying to communicate
+ * with a mod that is installed on the player's client. To send a plugin message to the server
+ * from the player, you should use the equivalent method on the instance returned by
+ * {@link #getCurrentServer()}.
+ *
+ * @inheritDoc
+ */
+ @Override
+ boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data);
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
index 787992fe0..f988f49e5 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
@@ -42,7 +42,7 @@ import com.velocitypowered.proxy.network.packet.clientbound.ClientboundSetCompre
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherDecoder;
import com.velocitypowered.proxy.network.pipeline.MinecraftCipherEncoder;
import com.velocitypowered.proxy.network.pipeline.MinecraftCompressDecoder;
-import com.velocitypowered.proxy.network.pipeline.MinecraftCompressEncoder;
+import com.velocitypowered.proxy.network.pipeline.MinecraftCompressorAndLengthEncoder;
import com.velocitypowered.proxy.network.pipeline.MinecraftDecoder;
import com.velocitypowered.proxy.network.pipeline.MinecraftEncoder;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
@@ -408,8 +408,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
} else {
MinecraftCompressDecoder decoder = (MinecraftCompressDecoder) channel.pipeline()
.get(COMPRESSION_DECODER);
- MinecraftCompressEncoder encoder = (MinecraftCompressEncoder) channel.pipeline()
- .get(COMPRESSION_ENCODER);
+ MinecraftCompressorAndLengthEncoder encoder =
+ (MinecraftCompressorAndLengthEncoder) channel.pipeline().get(COMPRESSION_ENCODER);
if (decoder != null && encoder != null) {
decoder.setThreshold(threshold);
encoder.setThreshold(threshold);
@@ -417,9 +417,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
int level = server.configuration().getCompressionLevel();
VelocityCompressor compressor = Natives.compress.get().create(level);
- encoder = new MinecraftCompressEncoder(threshold, compressor);
+ encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
decoder = new MinecraftCompressDecoder(threshold, compressor);
+ channel.pipeline().remove(FRAME_ENCODER);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
index dc3184832..944500e5f 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
@@ -59,12 +59,16 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class);
private static final boolean BACKPRESSURE_LOG = Boolean
.getBoolean("velocity.log-server-backpressure");
+ private static final int MAXIMUM_PACKETS_TO_FLUSH = Integer
+ .getInteger("velocity.max-packets-per-flush", 8192);
+
private final VelocityServer server;
private final VelocityServerConnection serverConn;
private final ClientPlaySessionHandler playerSessionHandler;
private final MinecraftConnection playerConnection;
private final BungeeCordMessageResponder bungeecordMessageResponder;
private boolean exceptionTriggered = false;
+ private int packetsFlushed;
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
this.server = server;
@@ -274,16 +278,25 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
((AbstractPluginMessagePacket>) packet).retain();
}
playerConnection.delayedWrite(packet);
+ if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
+ playerConnection.flush();
+ packetsFlushed = 0;
+ }
}
@Override
public void handleUnknown(ByteBuf buf) {
playerConnection.delayedWrite(buf.retain());
+ if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
+ playerConnection.flush();
+ packetsFlushed = 0;
+ }
}
@Override
public void readCompleted() {
playerConnection.flush();
+ packetsFlushed = 0;
}
@Override
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java
index 8be7316fa..32cb1bf34 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ProtocolUtils.java
@@ -59,7 +59,15 @@ public enum ProtocolUtils {
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
private static final QuietDecoderException BAD_VARINT_CACHED =
- new QuietDecoderException("Bad varint decoded");
+ new QuietDecoderException("Bad VarInt decoded");
+ private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
+
+ static {
+ for (int i = 0; i <= 32; ++i) {
+ VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
+ }
+ VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0.
+ }
/**
* Reads a Minecraft-style VarInt from the specified {@code buf}.
@@ -69,7 +77,7 @@ public enum ProtocolUtils {
public static int readVarInt(ByteBuf buf) {
int read = readVarIntSafely(buf);
if (read == Integer.MIN_VALUE) {
- throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad varint decoded")
+ throw MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
: BAD_VARINT_CACHED;
}
return read;
@@ -95,23 +103,67 @@ public enum ProtocolUtils {
return Integer.MIN_VALUE;
}
+ /**
+ * Returns the exact byte size of {@code value} if it were encoded as a VarInt.
+ * @param value the value to encode
+ * @return the byte size of {@code value} if encoded as a VarInt
+ */
+ public static int varIntBytes(int value) {
+ return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
+ }
+
/**
* Writes a Minecraft-style VarInt to the specified {@code buf}.
* @param buf the buffer to read from
* @param value the integer to write
*/
public static void writeVarInt(ByteBuf buf, int value) {
- while (true) {
- if ((value & 0xFFFFFF80) == 0) {
- buf.writeByte(value);
- return;
- }
-
- buf.writeByte(value & 0x7F | 0x80);
- value >>>= 7;
+ // Peel the one and two byte count cases explicitly as they are the most common VarInt sizes
+ // that the proxy will write, to improve inlining.
+ if ((value & (0xFFFFFFFF << 7)) == 0) {
+ buf.writeByte(value);
+ } else if ((value & (0xFFFFFFFF << 14)) == 0) {
+ int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
+ buf.writeShort(w);
+ } else {
+ writeVarIntFull(buf, value);
}
}
+ private static void writeVarIntFull(ByteBuf buf, int value) {
+ // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
+ if ((value & (0xFFFFFFFF << 7)) == 0) {
+ buf.writeByte(value);
+ } else if ((value & (0xFFFFFFFF << 14)) == 0) {
+ int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
+ buf.writeShort(w);
+ } else if ((value & (0xFFFFFFFF << 21)) == 0) {
+ int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
+ buf.writeMedium(w);
+ } else if ((value & (0xFFFFFFFF << 28)) == 0) {
+ int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16)
+ | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21);
+ buf.writeInt(w);
+ } else {
+ int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
+ | ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
+ buf.writeInt(w);
+ buf.writeByte(value >>> 28);
+ }
+ }
+
+ /**
+ * Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}.
+ * The upper 11 bits will be discarded.
+ * @param buf the buffer to read from
+ * @param value the integer to write
+ */
+ public static void write21BitVarInt(ByteBuf buf, int value) {
+ // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
+ int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
+ buf.writeMedium(w);
+ }
+
public static String readString(ByteBuf buf) {
return readString(buf, DEFAULT_MAX_STRING_SIZE);
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java
deleted file mode 100644
index b482b7320..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressEncoder.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.velocitypowered.proxy.network.pipeline;
-
-import com.velocitypowered.natives.compression.VelocityCompressor;
-import com.velocitypowered.natives.util.MoreByteBufUtils;
-import com.velocitypowered.proxy.network.ProtocolUtils;
-import io.netty.buffer.ByteBuf;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.handler.codec.MessageToByteEncoder;
-
-public class MinecraftCompressEncoder extends MessageToByteEncoder {
-
- private int threshold;
- private final VelocityCompressor compressor;
-
- public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
- this.threshold = threshold;
- this.compressor = compressor;
- }
-
- @Override
- protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
- int uncompressed = msg.readableBytes();
- if (uncompressed < threshold) {
- // Under the threshold, there is nothing to do.
- ProtocolUtils.writeVarInt(out, 0);
- out.writeBytes(msg);
- } else {
- ProtocolUtils.writeVarInt(out, uncompressed);
- ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
- try {
- compressor.deflate(compatibleIn, out);
- } finally {
- compatibleIn.release();
- }
- }
- }
-
- @Override
- protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
- throws Exception {
- // We allocate bytes to be compressed plus 1 byte. This covers two cases:
- //
- // - Compression
- // According to https://github.com/ebiggers/libdeflate/blob/master/libdeflate.h#L103,
- // if the data compresses well (and we do not have some pathological case) then the maximum
- // size the compressed size will ever be is the input size minus one.
- // - Uncompressed
- // This is fairly obvious - we will then have one more than the uncompressed size.
- int initialBufferSize = msg.readableBytes() + 1;
- return MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
- }
-
- @Override
- public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
- compressor.close();
- }
-
- public void setThreshold(int threshold) {
- this.threshold = threshold;
- }
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressorAndLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressorAndLengthEncoder.java
new file mode 100644
index 000000000..28d005161
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftCompressorAndLengthEncoder.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.protocol.netty;
+
+import static com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder.IS_JAVA_CIPHER;
+
+import com.velocitypowered.natives.compression.VelocityCompressor;
+import com.velocitypowered.natives.util.MoreByteBufUtils;
+import com.velocitypowered.proxy.protocol.ProtocolUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+import java.util.zip.DataFormatException;
+
+public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder {
+
+ private static final boolean MUST_USE_SAFE_AND_SLOW_COMPRESSION_HANDLING =
+ Boolean.getBoolean("velocity.increased-compression-cap");
+
+ private int threshold;
+ private final VelocityCompressor compressor;
+
+ public MinecraftCompressorAndLengthEncoder(int threshold, VelocityCompressor compressor) {
+ this.threshold = threshold;
+ this.compressor = compressor;
+ }
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
+ int uncompressed = msg.readableBytes();
+ if (uncompressed < threshold) {
+ // Under the threshold, there is nothing to do.
+ ProtocolUtils.writeVarInt(out, uncompressed + 1);
+ ProtocolUtils.writeVarInt(out, 0);
+ out.writeBytes(msg);
+ } else {
+ if (MUST_USE_SAFE_AND_SLOW_COMPRESSION_HANDLING) {
+ handleCompressedSafe(ctx, msg, out);
+ } else {
+ handleCompressedFast(ctx, msg, out);
+ }
+ }
+ }
+
+ private void handleCompressedFast(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out)
+ throws DataFormatException {
+ int uncompressed = msg.readableBytes();
+
+ ProtocolUtils.write21BitVarInt(out, 0); // Dummy packet length
+ ProtocolUtils.writeVarInt(out, uncompressed);
+ ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
+
+ int startCompressed = out.writerIndex();
+ try {
+ compressor.deflate(compatibleIn, out);
+ } finally {
+ compatibleIn.release();
+ }
+ int compressedLength = out.writerIndex() - startCompressed;
+ if (compressedLength >= 1 << 21) {
+ throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet. "
+ + "Please restart Velocity with the JVM flag -Dvelocity.increased-compression-cap=true "
+ + "to fix this issue.");
+ }
+
+ int writerIndex = out.writerIndex();
+ int packetLength = out.readableBytes() - 3;
+ out.writerIndex(0);
+ ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length
+ out.writerIndex(writerIndex);
+ }
+
+ private void handleCompressedSafe(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out)
+ throws DataFormatException {
+ int uncompressed = msg.readableBytes();
+ ByteBuf tmpBuf = MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, uncompressed - 1);
+ try {
+ ProtocolUtils.writeVarInt(tmpBuf, uncompressed);
+ ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
+ try {
+ compressor.deflate(compatibleIn, tmpBuf);
+ } finally {
+ compatibleIn.release();
+ }
+
+ ProtocolUtils.writeVarInt(out, tmpBuf.readableBytes());
+ out.writeBytes(tmpBuf);
+ } finally {
+ tmpBuf.release();
+ }
+ }
+
+ @Override
+ protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
+ throws Exception {
+ int uncompressed = msg.readableBytes();
+ if (uncompressed < threshold) {
+ int finalBufferSize = uncompressed + 1;
+ finalBufferSize += ProtocolUtils.varIntBytes(finalBufferSize);
+ return IS_JAVA_CIPHER
+ ? ctx.alloc().heapBuffer(finalBufferSize)
+ : ctx.alloc().directBuffer(finalBufferSize);
+ }
+
+ // (maximum data length after compression) + packet length varint + uncompressed data varint
+ int initialBufferSize = (uncompressed - 1) + 3 + ProtocolUtils.varIntBytes(uncompressed);
+ return MoreByteBufUtils.preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
+ }
+
+ @Override
+ public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+ compressor.close();
+ }
+
+ public void setThreshold(int threshold) {
+ this.threshold = threshold;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java
index fc568602b..6ef4df737 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftDecoder.java
@@ -63,7 +63,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
}
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
- if (!ctx.channel().isActive()) {
+ if (!ctx.channel().isActive() || !buf.isReadable()) {
buf.release();
return;
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftVarintLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftVarintLengthEncoder.java
index cfd66f505..91645e9ea 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftVarintLengthEncoder.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/pipeline/MinecraftVarintLengthEncoder.java
@@ -29,7 +29,7 @@ import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftVarintLengthEncoder extends MessageToByteEncoder {
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
- private static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY;
+ public static final boolean IS_JAVA_CIPHER = Natives.cipher.get() == JavaVelocityCipher.FACTORY;
private MinecraftVarintLengthEncoder() {
}
@@ -43,7 +43,8 @@ public class MinecraftVarintLengthEncoder extends MessageToByteEncoder
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect)
throws Exception {
- int anticipatedRequiredCapacity = 5 + msg.readableBytes();
+ int anticipatedRequiredCapacity = ProtocolUtils.varIntBytes(msg.readableBytes())
+ + msg.readableBytes();
return IS_JAVA_CIPHER
? ctx.alloc().heapBuffer(anticipatedRequiredCapacity)
: ctx.alloc().directBuffer(anticipatedRequiredCapacity);
diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java
new file mode 100644
index 000000000..04b2f3193
--- /dev/null
+++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/ProtocolUtilsTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.protocol;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.Test;
+
+public class ProtocolUtilsTest {
+
+ @Test
+ void negativeVarIntBytes() {
+ assertEquals(5, ProtocolUtils.varIntBytes(-1));
+ assertEquals(5, ProtocolUtils.varIntBytes(Integer.MIN_VALUE));
+ }
+
+ @Test
+ void zeroVarIntBytes() {
+ assertEquals(1, ProtocolUtils.varIntBytes(0));
+ assertEquals(1, ProtocolUtils.varIntBytes(1));
+ }
+
+ @Test
+ void ensureConsistencyAcrossNumberBits() {
+ for (int i = 0; i <= 31; i++) {
+ int number = (1 << i) - 1;
+ assertEquals(conventionalWrittenBytes(number), ProtocolUtils.varIntBytes(number),
+ "mismatch with " + i + "-bit number");
+ }
+ }
+
+ @Test
+ void testPositiveOld() {
+ ByteBuf buf = Unpooled.buffer(5);
+ for (int i = 0; i >= 0; i += 127) {
+ writeReadTestOld(buf, i);
+ }
+ }
+
+ @Test
+ void testNegativeOld() {
+ ByteBuf buf = Unpooled.buffer(5);
+ for (int i = 0; i <= 0; i -= 127) {
+ writeReadTestOld(buf, i);
+ }
+ }
+
+ private void writeReadTestOld(ByteBuf buf, int test) {
+ buf.clear();
+ writeVarIntOld(buf, test);
+ assertEquals(test, ProtocolUtils.readVarIntSafely(buf));
+ }
+
+ @Test
+ void test3Bytes() {
+ ByteBuf buf = Unpooled.buffer(5);
+ for (int i = 0; i < 2097152; i += 31) {
+ writeReadTest3Bytes(buf, i);
+ }
+ }
+
+ private void writeReadTest3Bytes(ByteBuf buf, int test) {
+ buf.clear();
+ ProtocolUtils.write21BitVarInt(buf, test);
+ assertEquals(test, ProtocolUtils.readVarInt(buf));
+ }
+
+ @Test
+ void testBytesWrittenAtBitBoundaries() {
+ ByteBuf varintNew = Unpooled.buffer(5);
+ ByteBuf varintOld = Unpooled.buffer(5);
+
+ long bytesNew = 0;
+ long bytesOld = 0;
+ for (int bit = 0; bit <= 31; bit++) {
+ int i = (1 << bit) - 1;
+
+ writeVarIntOld(varintOld, i);
+ ProtocolUtils.writeVarInt(varintNew, i);
+ assertArrayEquals(ByteBufUtil.getBytes(varintOld), ByteBufUtil.getBytes(varintNew),
+ "Encoding of " + i + " was invalid");
+
+ assertEquals(i, oldReadVarIntSafely(varintNew));
+ assertEquals(i, ProtocolUtils.readVarIntSafely(varintOld));
+
+ varintNew.clear();
+ varintOld.clear();
+ }
+ assertEquals(bytesNew, bytesOld, "byte sizes differ");
+ }
+
+ @Test
+ void testBytesWritten() {
+ ByteBuf varintNew = Unpooled.buffer(5);
+ ByteBuf varintOld = Unpooled.buffer(5);
+
+ long bytesNew = 0;
+ long bytesOld = 0;
+ for (int i = 0; i <= 1_000_000; i++) {
+ ProtocolUtils.writeVarInt(varintNew, i);
+ writeVarIntOld(varintOld, i);
+ bytesNew += varintNew.readableBytes();
+ bytesOld += varintOld.readableBytes();
+ varintNew.clear();
+ varintOld.clear();
+ }
+ assertEquals(bytesNew, bytesOld, "byte sizes differ");
+ }
+
+ private static int oldReadVarIntSafely(ByteBuf buf) {
+ int i = 0;
+ int maxRead = Math.min(5, buf.readableBytes());
+ for (int j = 0; j < maxRead; j++) {
+ int k = buf.readByte();
+ i |= (k & 0x7F) << j * 7;
+ if ((k & 0x80) != 128) {
+ return i;
+ }
+ }
+ return Integer.MIN_VALUE;
+ }
+
+ private void writeVarIntOld(ByteBuf buf, int value) {
+ while (true) {
+ if ((value & 0xFFFFFF80) == 0) {
+ buf.writeByte(value);
+ return;
+ }
+
+ buf.writeByte(value & 0x7F | 0x80);
+ value >>>= 7;
+ }
+ }
+
+ private int conventionalWrittenBytes(int value) {
+ int wouldBeWritten = 0;
+ while (true) {
+ if ((value & ~0x7FL) == 0) {
+ wouldBeWritten++;
+ return wouldBeWritten;
+ } else {
+ wouldBeWritten++;
+ value >>>= 7;
+ }
+ }
+ }
+}