From 6159507bb456bc592a11dfb8cd69a2434fa84072 Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Fri, 6 Dec 2013 18:02:26 +0100 Subject: [PATCH] Attempting to add backwards compatibility for NetworkMarker. --- .../protocol/events/NetworkMarker.java | 141 ++++++++++++++++-- .../protocol/events/PacketEvent.java | 4 +- .../protocol/events/PacketOutputHandler.java | 6 +- .../injector/netty/ChannelInjector.java | 6 +- .../injector/netty/NettyNetworkMarker.java | 40 +++++ .../injector/netty/NettyProtocolInjector.java | 2 +- .../injector/packet/LegacyNetworkMarker.java | 66 ++++++++ .../injector/packet/ProxyPacketInjector.java | 2 +- .../injector/spigot/SpigotPacketInjector.java | 3 +- .../utility/ByteBufferInputStream.java | 35 +++++ .../utility/ByteBufferOutputStream.java | 26 ++++ .../protocol/utility/StreamSerializer.java | 49 ++++++ 12 files changed, 359 insertions(+), 21 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyNetworkMarker.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/LegacyNetworkMarker.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferInputStream.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferOutputStream.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java index ce0b401d..85eac9ff 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/NetworkMarker.java @@ -2,6 +2,7 @@ package com.comphenix.protocol.events; import java.io.ByteArrayInputStream; import java.io.DataInputStream; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Collections; @@ -10,6 +11,9 @@ import java.util.PriorityQueue; import javax.annotation.Nonnull; +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.utility.ByteBufferInputStream; +import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.StreamSerializer; import com.google.common.base.Preconditions; import com.google.common.primitives.Ints; @@ -20,13 +24,35 @@ import com.google.common.primitives.Ints; * * @author Kristian */ -public class NetworkMarker { +public abstract class NetworkMarker { + public static class EmptyBufferMarker extends NetworkMarker { + public EmptyBufferMarker(@Nonnull ConnectionSide side) { + super(side, (ByteBuffer)null, null); + } + + @Override + protected DataInputStream skipHeader(DataInputStream input) throws IOException { + throw new IllegalStateException("Buffer is empty."); + } + + @Override + protected ByteBuffer addHeader(ByteBuffer buffer, PacketType type) { + throw new IllegalStateException("Buffer is empty."); + } + + @Override + protected DataInputStream addHeader(DataInputStream input, PacketType type) { + throw new IllegalStateException("Buffer is empty."); + } + } + // Custom network handler private PriorityQueue outputHandlers; + // The input buffer private ByteBuffer inputBuffer; - private final ConnectionSide side; + private final PacketType type; // Cache serializer too private StreamSerializer serializer; @@ -36,9 +62,10 @@ public class NetworkMarker { * @param side - whether or not this marker belongs to a client or server packet. * @param inputBuffer - the read serialized packet data. */ - public NetworkMarker(@Nonnull ConnectionSide side, ByteBuffer inputBuffer) { + public NetworkMarker(@Nonnull ConnectionSide side, ByteBuffer inputBuffer, PacketType type) { this.side = Preconditions.checkNotNull(side, "side cannot be NULL."); this.inputBuffer = Preconditions.checkNotNull(inputBuffer, "inputBuffer cannot be NULL."); + this.type = Preconditions.checkNotNull(type, "type cannot be NULL."); } /** @@ -47,10 +74,12 @@ public class NetworkMarker { * The input buffer is only non-null for client-side packets. * @param side - whether or not this marker belongs to a client or server packet. * @param inputBuffer - the read serialized packet data. + * @param handler - handle skipping headers. */ - public NetworkMarker(@Nonnull ConnectionSide side, byte[] inputBuffer) { + public NetworkMarker(@Nonnull ConnectionSide side, byte[] inputBuffer, PacketType type) { this.side = Preconditions.checkNotNull(side, "side cannot be NULL."); - + this.type = Preconditions.checkNotNull(type, "type cannot be NULL."); + if (inputBuffer != null) { this.inputBuffer = ByteBuffer.wrap(inputBuffer); } @@ -75,7 +104,7 @@ public class NetworkMarker { } /** - * Retrieve the serialized packet data (excluding the header) from the network input stream. + * Retrieve the serialized packet data (excluding the header by default) from the network input stream. *

* The returned buffer is read-only. If the parent event is a server side packet this * method throws {@link IllegalStateException}. @@ -84,9 +113,49 @@ public class NetworkMarker { * @return A byte buffer containing the raw packet data read from the network. */ public ByteBuffer getInputBuffer() { + return getInputBuffer(true); + } + + /** + * Retrieve the serialized packet data from the network input stream. + *

+ * The returned buffer is read-only. If the parent event is a server side packet this + * method throws {@link IllegalStateException}. + *

+ * It returns NULL if the packet was transmitted by a plugin locally. + * @param excludeHeader - whether or not to exclude the packet ID header. + * @return A byte buffer containing the raw packet data read from the network. + */ + public ByteBuffer getInputBuffer(boolean excludeHeader) { if (side.isForServer()) throw new IllegalStateException("Server-side packets have no input buffer."); - return inputBuffer != null ? inputBuffer.asReadOnlyBuffer() : null; + + if (inputBuffer != null) { + ByteBuffer result = inputBuffer.asReadOnlyBuffer(); + + try { + if (excludeHeader) + result = skipHeader(result); + else + result = addHeader(result, type); + } catch (IOException e) { + throw new RuntimeException("Cannot skip packet header.", e); + } + return result; + } + return null; + } + + /** + * Retrieve the serialized packet data (excluding the header by default) as an input stream. + *

+ * The data is exactly the same as in {@link #getInputBuffer()}. + * @see #getInputBuffer() + * @param excludeHeader - whether or not to exclude the packet ID header. + * @return The incoming serialized packet data as a stream, or NULL if the packet was transmitted locally. + */ + public DataInputStream getInputStream() { + return getInputStream(true); } /** @@ -94,17 +163,37 @@ public class NetworkMarker { *

* The data is exactly the same as in {@link #getInputBuffer()}. * @see #getInputBuffer() + * @param excludeHeader - whether or not to exclude the packet ID header. * @return The incoming serialized packet data as a stream, or NULL if the packet was transmitted locally. */ - public DataInputStream getInputStream() { + @SuppressWarnings("resource") + public DataInputStream getInputStream(boolean excludeHeader) { if (side.isForServer()) throw new IllegalStateException("Server-side packets have no input buffer."); if (inputBuffer == null) return null; - return new DataInputStream( + DataInputStream input = new DataInputStream( new ByteArrayInputStream(inputBuffer.array()) ); + + try { + if (excludeHeader) + input = skipHeader(input); + else + input = addHeader(input, type); + } catch (IOException e) { + throw new RuntimeException("Cannot skip packet header.", e); + } + return input; + } + + /** + * Whether or not the output handlers have to write a packet header. + * @return TRUE if they do, FALSE otherwise. + */ + public boolean requireOutputHeader() { + return MinecraftReflection.isUsingNetty(); } /** @@ -172,6 +261,40 @@ public class NetworkMarker { } } + /** + * Return a byte buffer without the header in the current packet. + *

+ * It's safe to modify the position of the buffer. + * @param buffer - a read-only byte source. + */ + protected ByteBuffer skipHeader(ByteBuffer buffer) throws IOException { + skipHeader(new DataInputStream(new ByteBufferInputStream(buffer))); + return buffer; + } + + /** + * Return an input stream without the header in the current packet. + *

+ * It's safe to modify the input stream. + */ + protected abstract DataInputStream skipHeader(DataInputStream input) throws IOException; + + /** + * Return the byte buffer prepended with the packet header. + * @param buffer - the read-only byte buffer. + * @param type - the current packet. + * @return The byte buffer. + */ + protected abstract ByteBuffer addHeader(ByteBuffer buffer, PacketType type); + + /** + * Return the input stream prepended with the packet header. + * @param input - the input stream. + * @param type - the current packet. + * @return The byte buffer. + */ + protected abstract DataInputStream addHeader(DataInputStream input, PacketType type); + /** * Determine if the given marker has any output handlers. * @param marker - the marker to check. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java index 69c2e96d..055f6ad6 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketEvent.java @@ -207,8 +207,8 @@ public class PacketEvent extends EventObject implements Cancellable { public NetworkMarker getNetworkMarker() { if (networkMarker == null) { if (isServerPacket()) { - networkMarker = new NetworkMarker( - serverPacket ? ConnectionSide.SERVER_SIDE : ConnectionSide.CLIENT_SIDE, (byte[]) null); + networkMarker = new NetworkMarker.EmptyBufferMarker( + serverPacket ? ConnectionSide.SERVER_SIDE : ConnectionSide.CLIENT_SIDE); } else { throw new IllegalStateException("Add the option ListenerOptions.INTERCEPT_INPUT_BUFFER to your listener."); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketOutputHandler.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketOutputHandler.java index 4e9c4856..4e8f86b2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketOutputHandler.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketOutputHandler.java @@ -25,8 +25,10 @@ public interface PacketOutputHandler { /** * Invoked when a given packet is to be written to the output stream. *

- * Note that the buffer is initially filled with the output from the default write method. This excludes - * the packet ID header. + * Note that the buffer is initially filled with the output from the default write method. + *

+ * In Minecraft 1.6.4, the header is always excluded, whereas it MUST be included in Minecraft 1.7.2. Call + * {@link NetworkMarker#requireOutputHeader()} to determine this. * @param event - the packet that will be outputted. * @param buffer - the data that is currently scheduled to be outputted. * @return The modified byte array to write. NULL is not permitted. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java index af23d972..d4762317 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/ChannelInjector.java @@ -244,10 +244,6 @@ class ChannelInjector extends ByteToMessageDecoder { protected void encode(ChannelHandlerContext ctx, Object packet, ByteBuf output) throws Exception { ChannelInjector.this.encode(ctx, packet, output); } - - public void exceptionCaught(ChannelHandlerContext channelhandlercontext, Throwable throwable) { - throwable.printStackTrace(); - } }; // Insert our handlers - note that we effectively replace the vanilla encoder/decoder @@ -369,7 +365,7 @@ class ChannelInjector extends ByteToMessageDecoder { if (channelListener.includeBuffer(packetClass)) { byteBuffer.resetReaderIndex(); - marker = new NetworkMarker(ConnectionSide.CLIENT_SIDE, getBytes(byteBuffer)); + marker = new NettyNetworkMarker(ConnectionSide.CLIENT_SIDE, getBytes(byteBuffer)); } Object output = channelListener.onPacketReceiving(this, input, marker); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyNetworkMarker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyNetworkMarker.java new file mode 100644 index 00000000..a67f9983 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyNetworkMarker.java @@ -0,0 +1,40 @@ +package com.comphenix.protocol.injector.netty; + +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import javax.annotation.Nonnull; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.NetworkMarker; + +class NettyNetworkMarker extends NetworkMarker { + public NettyNetworkMarker(@Nonnull ConnectionSide side, byte[] inputBuffer) { + super(side, inputBuffer, null); + } + + public NettyNetworkMarker(@Nonnull ConnectionSide side, ByteBuffer inputBuffer) { + super(side, inputBuffer, null); + } + + @Override + protected DataInputStream skipHeader(DataInputStream input) throws IOException { + // Skip the variable int containing the packet ID + getSerializer().deserializeVarInt(input); + return input; + } + + @Override + protected ByteBuffer addHeader(ByteBuffer buffer, PacketType type) { + // We don't have to add anything - it's already there + return buffer; + } + + @Override + protected DataInputStream addHeader(DataInputStream input, PacketType type) { + // As above + return input; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyProtocolInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyProtocolInjector.java index 16c5d53e..738bf706 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyProtocolInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/netty/NettyProtocolInjector.java @@ -307,7 +307,7 @@ public class NettyProtocolInjector implements ChannelListener { return new AbstractPacketInjector(reveivedFilters) { @Override public PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered) { - NetworkMarker marker = buffered != null ? new NetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; + NetworkMarker marker = buffered != null ? new NettyNetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; ChannelInjector.fromPlayer(client, NettyProtocolInjector.this). saveMarker(packet.getHandle(), marker); return packetReceived(packet, client, marker); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/LegacyNetworkMarker.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/LegacyNetworkMarker.java new file mode 100644 index 00000000..9769446c --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/LegacyNetworkMarker.java @@ -0,0 +1,66 @@ +package com.comphenix.protocol.injector.packet; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import javax.annotation.Nonnull; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.NetworkMarker; +import com.google.common.io.ByteStreams; +import com.google.common.io.InputSupplier; +import com.google.common.primitives.Bytes; + +/** + * Represents a network marker for 1.6.4 and earlier. + * @author Kristian + */ +public class LegacyNetworkMarker extends NetworkMarker { + public LegacyNetworkMarker(@Nonnull ConnectionSide side, byte[] inputBuffer, PacketType type) { + super(side, inputBuffer, type); + } + + public LegacyNetworkMarker(@Nonnull ConnectionSide side, ByteBuffer inputBuffer, PacketType type) { + super(side, inputBuffer, type); + } + + @Override + protected DataInputStream skipHeader(DataInputStream input) throws IOException { + // This has already been done + return input; + } + + @Override + protected ByteBuffer addHeader(ByteBuffer buffer, PacketType type) { + return ByteBuffer.wrap(Bytes.concat(new byte[] { (byte) type.getLegacyId() }, buffer.array())); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + protected DataInputStream addHeader(final DataInputStream input, final PacketType type) { + InputSupplier header = new InputSupplier() { + @Override + public InputStream getInput() throws IOException { + byte[] data = new byte[] { (byte) type.getLegacyId() }; + return new ByteArrayInputStream(data); + } + }; + + InputSupplier data = new InputSupplier() { + @Override + public InputStream getInput() throws IOException { + return input; + } + }; + + // Combine them into a single stream + try { + return new DataInputStream(ByteStreams.join((InputSupplier) header, (InputSupplier) data).getInput()); + } catch (IOException e) { + throw new RuntimeException("Cannot add header.", e); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java index e74000f0..e005c9f8 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/packet/ProxyPacketInjector.java @@ -349,7 +349,7 @@ class ProxyPacketInjector implements PacketInjector { @Override public PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered) { - NetworkMarker marker = buffered != null ? new NetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; + NetworkMarker marker = buffered != null ? new LegacyNetworkMarker(ConnectionSide.CLIENT_SIDE, buffered, packet.getType()) : null; PacketEvent event = PacketEvent.fromClient((Object) manager, packet, marker, client); manager.invokePacketRecieving(event); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java index 35560175..f378fda2 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/injector/spigot/SpigotPacketInjector.java @@ -35,6 +35,7 @@ import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.injector.ListenerInvoker; import com.comphenix.protocol.injector.PlayerLoggedOutException; +import com.comphenix.protocol.injector.packet.LegacyNetworkMarker; import com.comphenix.protocol.injector.packet.PacketInjector; import com.comphenix.protocol.injector.player.NetworkObjectInjector; import com.comphenix.protocol.injector.player.PlayerInjectionHandler; @@ -486,7 +487,7 @@ public class SpigotPacketInjector implements SpigotPacketListener { * @return The packet event that was used. */ PacketEvent packetReceived(PacketContainer packet, Player sender, byte[] buffered) { - NetworkMarker marker = buffered != null ? new NetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; + NetworkMarker marker = buffered != null ? new LegacyNetworkMarker(ConnectionSide.CLIENT_SIDE, buffered, packet.getType()) : null; PacketEvent event = PacketEvent.fromClient(this, packet, marker, sender); invoker.invokePacketRecieving(event); diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferInputStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferInputStream.java new file mode 100644 index 00000000..ce730f12 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferInputStream.java @@ -0,0 +1,35 @@ +package com.comphenix.protocol.utility; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Represents an input stream that delegates to a byte buffer. + * @author Kristian + */ +public class ByteBufferInputStream extends InputStream { + private ByteBuffer buf; + + public ByteBufferInputStream(ByteBuffer buf) { + this.buf = buf; + } + + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + public int read(byte[] bytes, int off, int len) + throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferOutputStream.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferOutputStream.java new file mode 100644 index 00000000..89d37879 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ByteBufferOutputStream.java @@ -0,0 +1,26 @@ +package com.comphenix.protocol.utility; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Represents an output stream that is backed by a ByteBuffer. + * @author Kristian + */ +public class ByteBufferOutputStream extends OutputStream { + ByteBuffer buf; + + public ByteBufferOutputStream(ByteBuffer buf) { + this.buf = buf; + } + + public void write(int b) throws IOException { + buf.put((byte) b); + } + + public void write(byte[] bytes, int off, int len) + throws IOException { + buf.put(bytes, off, len); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java index 968901f8..3e630648 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/StreamSerializer.java @@ -17,6 +17,7 @@ import com.comphenix.protocol.reflect.FuzzyReflection.MethodAccessor; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; import com.comphenix.protocol.wrappers.nbt.NbtCompound; import com.comphenix.protocol.wrappers.nbt.NbtFactory; +import com.google.common.base.Preconditions; /** * Utility methods for reading and writing Minecraft objects to streams. @@ -24,6 +25,8 @@ import com.comphenix.protocol.wrappers.nbt.NbtFactory; * @author Kristian */ public class StreamSerializer { + private static final StreamSerializer DEFAULT = new StreamSerializer(); + // Cached methods private static MethodAccessor READ_ITEM_METHOD; private static MethodAccessor WRITE_ITEM_METHOD; @@ -34,6 +37,52 @@ public class StreamSerializer { private static MethodAccessor READ_STRING_METHOD; private static MethodAccessor WRITE_STRING_METHOD; + /** + * Retrieve a default stream serializer. + * @return A serializer. + */ + public static StreamSerializer getDefault() { + return DEFAULT; + } + + /** + * Read a variable integer from an input stream. + * @param source - the source. + * @return The integer. + * @throws IOException The source stream threw an exception. + */ + public int deserializeVarInt(@Nonnull DataInputStream source) throws IOException { + Preconditions.checkNotNull(source, "source cannot be NULL"); + + int result = 0; + int length = 0; + byte currentByte; + do { + currentByte = source.readByte(); + result |= (currentByte & 0x7F) << length++ * 7; + if (length > 5) + throw new RuntimeException("VarInt too big"); + } while ((currentByte & 0x80) == 0x80); + + return result; + } + + /** + * Write a variable integer to an output stream. + * @param destination - the destination. + * @param value - the value to write. + * @throws IOException The destination stream threw an exception. + */ + public void serializeVarInt(@Nonnull DataOutputStream destination, int value) throws IOException { + Preconditions.checkNotNull(destination, "source cannot be NULL"); + + while ((value & 0xFFFFFF80) != 0) { + destination.writeByte(value & 0x7F | 0x80); + value >>>= 7; + } + destination.writeByte(value); + } + /** * Read or deserialize an item stack from an underlying input stream. *