diff --git a/src/main/java/net/minestom/server/extras/query/response/BasicQueryResponse.java b/src/main/java/net/minestom/server/extras/query/response/BasicQueryResponse.java index 144c30064..fd4e2d512 100644 --- a/src/main/java/net/minestom/server/extras/query/response/BasicQueryResponse.java +++ b/src/main/java/net/minestom/server/extras/query/response/BasicQueryResponse.java @@ -142,7 +142,7 @@ public class BasicQueryResponse implements Writeable { writer.writeNullTerminatedString(this.map, Query.CHARSET); writer.writeNullTerminatedString(this.numPlayers, Query.CHARSET); writer.writeNullTerminatedString(this.maxPlayers, Query.CHARSET); - writer.getBuffer().putShort((short) MinecraftServer.getServer().getPort()); // TODO little endian? + writer.writeShort((short) MinecraftServer.getServer().getPort()); // TODO little endian? writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), ""), Query.CHARSET); } } diff --git a/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java b/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java index c0a2e3c7e..53e61842c 100644 --- a/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java +++ b/src/main/java/net/minestom/server/extras/velocity/VelocityProxy.java @@ -1,19 +1,17 @@ package net.minestom.server.extras.velocity; -import net.minestom.server.MinecraftServer; -import net.minestom.server.utils.binary.BinaryReader; +import net.minestom.server.network.NetworkBuffer; import org.jetbrains.annotations.NotNull; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import static net.minestom.server.network.NetworkBuffer.*; + /** * Support for Velocity modern forwarding. *

@@ -47,12 +45,14 @@ public final class VelocityProxy { return enabled; } - public static boolean checkIntegrity(@NotNull BinaryReader reader) { - final byte[] signature = reader.readBytes(32); - ByteBuffer buf = reader.getBuffer(); - buf.mark(); - final byte[] data = reader.readRemainingBytes(); - buf.reset(); + public static boolean checkIntegrity(NetworkBuffer buffer) { + final byte[] signature = new byte[32]; + for (int i = 0; i < signature.length; i++) { + signature[i] = buffer.read(BYTE); + } + final int index = buffer.readIndex(); + final byte[] data = buffer.read(RAW_BYTES); + buffer.readIndex(index); try { Mac mac = Mac.getInstance(MAC_ALGORITHM); mac.init(key); @@ -63,16 +63,7 @@ public final class VelocityProxy { } catch (NoSuchAlgorithmException | InvalidKeyException e) { e.printStackTrace(); } - final int version = reader.readVarInt(); + final int version = buffer.read(VAR_INT); return version == SUPPORTED_FORWARDING_VERSION; } - - public static InetAddress readAddress(@NotNull BinaryReader reader) { - try { - return InetAddress.getByName(reader.readSizedString()); - } catch (UnknownHostException e) { - MinecraftServer.getExceptionManager().handleException(e); - return null; - } - } } diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 6b7230f3d..95a62ec9d 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -20,7 +20,6 @@ import net.minestom.server.snapshot.SnapshotUpdater; import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.ObjectPool; -import net.minestom.server.utils.Utils; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.world.biomes.Biome; @@ -200,8 +199,8 @@ public class DynamicChunk extends Chunk { } final int bitsForHeight = MathUtils.bitsToRepresent(dimensionHeight); heightmapsNBT = NBT.Compound(Map.of( - "MOTION_BLOCKING", NBT.LongArray(Utils.encodeBlocks(motionBlocking, bitsForHeight)), - "WORLD_SURFACE", NBT.LongArray(Utils.encodeBlocks(worldSurface, bitsForHeight)))); + "MOTION_BLOCKING", NBT.LongArray(encodeBlocks(motionBlocking, bitsForHeight)), + "WORLD_SURFACE", NBT.LongArray(encodeBlocks(worldSurface, bitsForHeight)))); } // Data final byte[] data = ObjectPool.PACKET_POOL.use(buffer -> { @@ -265,4 +264,47 @@ public class DynamicChunk extends Chunk { private void assertLock() { assert Thread.holdsLock(this) : "Chunk must be locked before access"; } + + private static final int[] MAGIC = { + -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, + 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, + 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, + 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, + 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, + 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, + 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, + 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, + 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, + 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, + 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, + 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, + 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, + 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, + 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, + 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, + 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, + 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, + 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, + 0, 5}; + + private static long[] encodeBlocks(int[] blocks, int bitsPerEntry) { + final long maxEntryValue = (1L << bitsPerEntry) - 1; + final char valuesPerLong = (char) (64 / bitsPerEntry); + final int magicIndex = 3 * (valuesPerLong - 1); + final long divideMul = Integer.toUnsignedLong(MAGIC[magicIndex]); + final long divideAdd = Integer.toUnsignedLong(MAGIC[magicIndex + 1]); + final int divideShift = MAGIC[magicIndex + 2]; + final int size = (blocks.length + valuesPerLong - 1) / valuesPerLong; + + long[] data = new long[size]; + + for (int i = 0; i < blocks.length; i++) { + final long value = blocks[i]; + final int cellIndex = (int) (i * divideMul + divideAdd >> 32L >> divideShift); + final int bitIndex = (i - cellIndex * valuesPerLong) * bitsPerEntry; + data[cellIndex] = data[cellIndex] & ~(maxEntryValue << bitIndex) | (value & maxEntryValue) << bitIndex; + } + + return data; + } } diff --git a/src/main/java/net/minestom/server/item/ItemMetaImpl.java b/src/main/java/net/minestom/server/item/ItemMetaImpl.java index 192f2e490..4cde432d8 100644 --- a/src/main/java/net/minestom/server/item/ItemMetaImpl.java +++ b/src/main/java/net/minestom/server/item/ItemMetaImpl.java @@ -42,10 +42,7 @@ record ItemMetaImpl(TagHandler tagHandler) implements ItemMeta { writer.writeByte((byte) 0); return; } - BinaryWriter w = new BinaryWriter(); - w.writeNBT("", nbt); - var cachedBuffer = w.getBuffer(); - writer.write(cachedBuffer.flip()); + writer.writeNBT("", nbt); } @Override diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index 1d79c83a3..1642dbf93 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -1,13 +1,14 @@ package net.minestom.server.item; +import net.kyori.adventure.nbt.api.BinaryTagHolder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEventSource; +import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.tag.TagReadable; import net.minestom.server.tag.TagWritable; -import net.minestom.server.utils.NBTUtils; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.*; import org.jglrxavpok.hephaistos.nbt.NBTCompound; @@ -167,8 +168,8 @@ public sealed interface ItemStack extends TagReadable, HoverEventSource asHoverEvent(@NotNull UnaryOperator op) { - return HoverEvent.showItem(op.apply(HoverEvent.ShowItem.of(material(), amount(), - NBTUtils.asBinaryTagHolder(meta().toNBT())))); + final BinaryTagHolder tagHolder = BinaryTagHolder.encode(meta().toNBT(), MinestomAdventure.NBT_CODEC); + return HoverEvent.showItem(op.apply(HoverEvent.ShowItem.of(material(), amount(), tagHolder))); } /** diff --git a/src/main/java/net/minestom/server/network/NetworkBuffer.java b/src/main/java/net/minestom/server/network/NetworkBuffer.java new file mode 100644 index 000000000..c7e1fae75 --- /dev/null +++ b/src/main/java/net/minestom/server/network/NetworkBuffer.java @@ -0,0 +1,158 @@ +package net.minestom.server.network; + +import net.kyori.adventure.text.Component; +import net.minestom.server.coordinate.Point; +import net.minestom.server.item.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.jglrxavpok.hephaistos.nbt.NBTReader; +import org.jglrxavpok.hephaistos.nbt.NBTWriter; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +@ApiStatus.Experimental +public final class NetworkBuffer { + public static final Type BOOLEAN = NetworkBufferTypes.BOOLEAN; + public static final Type BYTE = NetworkBufferTypes.BYTE; + public static final Type SHORT = NetworkBufferTypes.SHORT; + public static final Type INT = NetworkBufferTypes.INT; + public static final Type LONG = NetworkBufferTypes.LONG; + public static final Type FLOAT = NetworkBufferTypes.FLOAT; + public static final Type DOUBLE = NetworkBufferTypes.DOUBLE; + public static final Type VAR_INT = NetworkBufferTypes.VAR_INT; + public static final Type VAR_LONG = NetworkBufferTypes.VAR_LONG; + public static final Type RAW_BYTES = NetworkBufferTypes.RAW_BYTES; + public static final Type STRING = NetworkBufferTypes.STRING; + public static final Type NBT = NetworkBufferTypes.NBT; + public static final Type BLOCK_POSITION = NetworkBufferTypes.BLOCK_POSITION; + public static final Type COMPONENT = NetworkBufferTypes.COMPONENT; + public static final Type UUID = NetworkBufferTypes.UUID; + public static final Type ITEM = NetworkBufferTypes.ITEM; + + ByteBuffer nioBuffer; + final boolean resizable; + int writeIndex; + int readIndex; + + NBTWriter nbtWriter; + NBTReader nbtReader; + + public NetworkBuffer(@NotNull ByteBuffer buffer, boolean resizable) { + this.nioBuffer = buffer.order(ByteOrder.BIG_ENDIAN); + this.resizable = resizable; + + this.writeIndex = buffer.position(); + this.readIndex = buffer.position(); + } + + public NetworkBuffer(@NotNull ByteBuffer buffer) { + this(buffer, true); + } + + public NetworkBuffer(int initialCapacity) { + this(ByteBuffer.allocateDirect(initialCapacity), true); + } + + public NetworkBuffer() { + this(1024); + } + + public void write(@NotNull Type type, @NotNull T value) { + var impl = (NetworkBufferTypes.TypeImpl) type; + final long length = impl.writer().write(this, value); + if (length != -1) this.writeIndex += length; + } + + public @NotNull T read(@NotNull Type type) { + var impl = (NetworkBufferTypes.TypeImpl) type; + return impl.reader().read(this); + } + + public void writeOptional(@NotNull Type type, @Nullable T value) { + write(BOOLEAN, value != null); + if (value != null) write(type, value); + } + + public @Nullable T readOptional(@NotNull Type type) { + return read(BOOLEAN) ? read(type) : null; + } + + public void writeCollection(@NotNull Type type, @Nullable Collection<@NotNull T> values) { + if (values == null) { + write(BYTE, (byte) 0); + return; + } + write(VAR_INT, values.size()); + for (T value : values) { + write(type, value); + } + } + + @SafeVarargs + public final void writeCollection(@NotNull Type type, @NotNull T @Nullable ... values) { + writeCollection(type, values == null ? null : List.of(values)); + } + + public @NotNull Collection<@NotNull T> readCollection(@NotNull Type type) { + final int size = read(VAR_INT); + final Collection values = new java.util.ArrayList<>(size); + for (int i = 0; i < size; i++) { + values.add(read(type)); + } + return values; + } + + public void copyTo(int srcOffset, byte @NotNull [] dest, int destOffset, int length) { + this.nioBuffer.get(srcOffset, dest, destOffset, length); + } + + public void clear() { + this.writeIndex = 0; + this.readIndex = 0; + } + + public int writeIndex() { + return writeIndex; + } + + public int readIndex() { + return readIndex; + } + + public void writeIndex(int writeIndex) { + this.writeIndex = writeIndex; + } + + public void readIndex(int readIndex) { + this.readIndex = readIndex; + } + + public int skipWrite(int length) { + final int oldWriteIndex = writeIndex; + writeIndex += length; + return oldWriteIndex; + } + + public int readableBytes() { + return writeIndex - readIndex; + } + + void ensureSize(int length) { + if (!resizable) return; + if (nioBuffer.capacity() < writeIndex + length) { + ByteBuffer newBuffer = ByteBuffer.allocateDirect(nioBuffer.capacity() * 2); + nioBuffer.position(0); + newBuffer.put(nioBuffer); + nioBuffer = newBuffer.clear(); + } + } + + public sealed interface Type permits NetworkBufferTypes.TypeImpl { + } +} diff --git a/src/main/java/net/minestom/server/network/NetworkBufferTypes.java b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java new file mode 100644 index 000000000..419df3ecc --- /dev/null +++ b/src/main/java/net/minestom/server/network/NetworkBufferTypes.java @@ -0,0 +1,322 @@ +package net.minestom.server.network; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jglrxavpok.hephaistos.nbt.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +final class NetworkBufferTypes { + private static final int SEGMENT_BITS = 0x7F; + private static final int CONTINUE_BIT = 0x80; + + static final TypeImpl BOOLEAN = new TypeImpl<>(Boolean.class, + (buffer, value) -> { + buffer.ensureSize(1); + buffer.nioBuffer.put(buffer.writeIndex(), value ? (byte) 1 : (byte) 0); + return 1; + }, + buffer -> { + final byte value = buffer.nioBuffer.get(buffer.readIndex()); + buffer.readIndex += 1; + return value == 1; + }); + static final TypeImpl BYTE = new TypeImpl<>(Byte.class, + (buffer, value) -> { + buffer.ensureSize(1); + buffer.nioBuffer.put(buffer.writeIndex(), value); + return 1; + }, + buffer -> { + final byte value = buffer.nioBuffer.get(buffer.readIndex()); + buffer.readIndex += 1; + return value; + }); + static final TypeImpl SHORT = new TypeImpl<>(Short.class, + (buffer, value) -> { + buffer.ensureSize(2); + buffer.nioBuffer.putShort(buffer.writeIndex(), value); + return 2; + }, + buffer -> { + final short value = buffer.nioBuffer.getShort(buffer.readIndex()); + buffer.readIndex += 2; + return value; + }); + static final TypeImpl INT = new TypeImpl<>(Integer.class, + (buffer, value) -> { + buffer.ensureSize(4); + buffer.nioBuffer.putInt(buffer.writeIndex(), value); + return 4; + }, + buffer -> { + final int value = buffer.nioBuffer.getInt(buffer.readIndex()); + buffer.readIndex += 4; + return value; + }); + static final TypeImpl LONG = new TypeImpl<>(Long.class, + (buffer, value) -> { + buffer.ensureSize(8); + buffer.nioBuffer.putLong(buffer.writeIndex(), value); + return 8; + }, + buffer -> { + final long value = buffer.nioBuffer.getLong(buffer.readIndex()); + buffer.readIndex += 8; + return value; + }); + static final TypeImpl FLOAT = new TypeImpl<>(Float.class, + (buffer, value) -> { + buffer.ensureSize(4); + buffer.nioBuffer.putFloat(buffer.writeIndex(), value); + return 4; + }, + buffer -> { + final float value = buffer.nioBuffer.getFloat(buffer.readIndex()); + buffer.readIndex += 4; + return value; + }); + static final TypeImpl DOUBLE = new TypeImpl<>(Double.class, + (buffer, value) -> { + buffer.ensureSize(8); + buffer.nioBuffer.putDouble(buffer.writeIndex(), value); + return 8; + }, + buffer -> { + final double value = buffer.nioBuffer.getDouble(buffer.readIndex()); + buffer.readIndex += 8; + return value; + }); + static final TypeImpl VAR_INT = new TypeImpl<>(Integer.class, + (buffer, boxed) -> { + final int value = boxed; + final int index = buffer.writeIndex(); + var nio = buffer.nioBuffer; + if ((value & (0xFFFFFFFF << 7)) == 0) { + buffer.ensureSize(1); + nio.put(index, (byte) value); + return 1; + } else if ((value & (0xFFFFFFFF << 14)) == 0) { + buffer.ensureSize(2); + nio.putShort(index, (short) ((value & 0x7F | 0x80) << 8 | (value >>> 7))); + return 2; + } else if ((value & (0xFFFFFFFF << 21)) == 0) { + buffer.ensureSize(3); + nio.put(index, (byte) (value & 0x7F | 0x80)); + nio.put(index + 1, (byte) ((value >>> 7) & 0x7F | 0x80)); + nio.put(index + 2, (byte) (value >>> 14)); + return 3; + } else if ((value & (0xFFFFFFFF << 28)) == 0) { + buffer.ensureSize(4); + nio.putInt(index, (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16) + | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21)); + return 4; + } else { + buffer.ensureSize(5); + nio.putInt(index, (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16 + | ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80)); + nio.put(index + 4, (byte) (value >>> 28)); + return 5; + } + }, + buffer -> { + int index = buffer.readIndex(); + // https://github.com/jvm-profiling-tools/async-profiler/blob/a38a375dc62b31a8109f3af97366a307abb0fe6f/src/converter/one/jfr/JfrReader.java#L393 + int result = 0; + for (int shift = 0; ; shift += 7) { + byte b = buffer.nioBuffer.get(index++); + result |= (b & 0x7f) << shift; + if (b >= 0) { + buffer.readIndex += index - buffer.readIndex(); + return result; + } + } + }); + static final TypeImpl VAR_LONG = new TypeImpl<>(Long.class, + (buffer, value) -> { + buffer.ensureSize(10); + int size = 0; + while (true) { + if ((value & ~((long) SEGMENT_BITS)) == 0) { + buffer.nioBuffer.put(buffer.writeIndex() + size, (byte) value.intValue()); + return size + 1; + } + buffer.nioBuffer.put(buffer.writeIndex() + size, (byte) (value & SEGMENT_BITS | CONTINUE_BIT)); + size++; + // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone + value >>>= 7; + } + }, + buffer -> { + int length = 0; + + long value = 0; + int position = 0; + byte currentByte; + + while (true) { + currentByte = buffer.nioBuffer.get(buffer.readIndex() + length); + length++; + value |= (long) (currentByte & SEGMENT_BITS) << position; + if ((currentByte & CONTINUE_BIT) == 0) break; + position += 7; + if (position >= 64) throw new RuntimeException("VarLong is too big"); + } + buffer.readIndex += length; + return value; + }); + static final TypeImpl RAW_BYTES = new TypeImpl<>(byte[].class, + (buffer, value) -> { + buffer.ensureSize(value.length); + buffer.nioBuffer.put(buffer.writeIndex(), value); + return value.length; + }, + buffer -> { + final int limit = buffer.nioBuffer.limit(); + final int length = limit - buffer.readIndex(); + assert length > 0 : "Invalid remaining: " + length; + final byte[] bytes = new byte[length]; + buffer.nioBuffer.get(buffer.readIndex(), bytes); + buffer.readIndex += length; + return bytes; + }); + static final TypeImpl STRING = new TypeImpl<>(String.class, + (buffer, value) -> { + final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + buffer.write(VAR_INT, bytes.length); + buffer.write(RAW_BYTES, bytes); + return -1; + }, + buffer -> { + final int length = buffer.read(VAR_INT); + byte[] bytes = new byte[length]; + buffer.nioBuffer.get(buffer.readIndex(), bytes); + buffer.readIndex += length; + return new String(bytes, StandardCharsets.UTF_8); + }); + static final TypeImpl NBT = new TypeImpl<>(NBT.class, + (buffer, value) -> { + NBTWriter nbtWriter = buffer.nbtWriter; + if (nbtWriter == null) { + nbtWriter = new NBTWriter(new OutputStream() { + @Override + public void write(int b) { + buffer.write(BYTE, (byte) b); + } + }, CompressedProcesser.NONE); + buffer.nbtWriter = nbtWriter; + } + try { + nbtWriter.writeNamed("", value); + } catch (IOException e) { + throw new RuntimeException(e); + } + return -1; + }, + buffer -> { + NBTReader nbtReader = buffer.nbtReader; + if (nbtReader == null) { + nbtReader = new NBTReader(new InputStream() { + @Override + public int read() { + return buffer.read(BYTE); + } + }, CompressedProcesser.NONE); + buffer.nbtReader = nbtReader; + } + try { + return nbtReader.read(); + } catch (IOException | NBTException e) { + throw new RuntimeException(e); + } + }); + static final TypeImpl BLOCK_POSITION = new TypeImpl<>(Point.class, + (buffer, value) -> { + final int blockX = value.blockX(); + final int blockY = value.blockY(); + final int blockZ = value.blockZ(); + final long longPos = (((long) blockX & 0x3FFFFFF) << 38) | + (((long) blockZ & 0x3FFFFFF) << 12) | + ((long) blockY & 0xFFF); + buffer.write(LONG, longPos); + return -1; + }, + buffer -> { + final long value = buffer.read(LONG); + final int x = (int) (value >> 38); + final int y = (int) (value << 52 >> 52); + final int z = (int) (value << 26 >> 38); + return new Vec(x, y, z); + }); + static final TypeImpl COMPONENT = new TypeImpl<>(Component.class, + (buffer, value) -> { + final String json = GsonComponentSerializer.gson().serialize(value); + buffer.write(STRING, json); + return -1; + }, + buffer -> { + final String json = buffer.read(STRING); + return GsonComponentSerializer.gson().deserialize(json); + }); + static final TypeImpl UUID = new TypeImpl<>(UUID.class, + (buffer, value) -> { + buffer.write(LONG, value.getMostSignificantBits()); + buffer.write(LONG, value.getLeastSignificantBits()); + return -1; + }, + buffer -> { + final long mostSignificantBits = buffer.read(LONG); + final long leastSignificantBits = buffer.read(LONG); + return new UUID(mostSignificantBits, leastSignificantBits); + }); + static final TypeImpl ITEM = new TypeImpl<>(ItemStack.class, + (buffer, value) -> { + if (value.isAir()) { + buffer.write(BOOLEAN, false); + return -1; + } + buffer.write(BOOLEAN, true); + buffer.write(VAR_INT, value.material().id()); + buffer.write(BYTE, (byte) value.amount()); + buffer.write(NBT, value.meta().toNBT()); + return -1; + }, + buffer -> { + final boolean present = buffer.read(BOOLEAN); + if (!present) return ItemStack.AIR; + + final int id = buffer.read(VAR_INT); + final Material material = Material.fromId(id); + if (material == null) throw new RuntimeException("Unknown material id: " + id); + + final int amount = buffer.read(BYTE); + final NBT nbt = buffer.read(NBT); + if (!(nbt instanceof NBTCompound compound)) { + return ItemStack.of(material, amount); + } + + return ItemStack.fromNBT(material, compound, amount); + }); + + record TypeImpl(@NotNull Class type, + @NotNull TypeWriter writer, + @NotNull TypeReader reader) implements NetworkBuffer.Type { + } + + interface TypeWriter { + long write(@NotNull NetworkBuffer buffer, @NotNull T value); + } + + interface TypeReader { + @NotNull T read(@NotNull NetworkBuffer buffer); + } +} diff --git a/src/main/java/net/minestom/server/network/PacketProcessor.java b/src/main/java/net/minestom/server/network/PacketProcessor.java index 5d5f5fd0d..cf810bd24 100644 --- a/src/main/java/net/minestom/server/network/PacketProcessor.java +++ b/src/main/java/net/minestom/server/network/PacketProcessor.java @@ -27,16 +27,19 @@ public record PacketProcessor(@NotNull ClientPacketsHandler statusHandler, } public @NotNull ClientPacket create(@NotNull ConnectionState connectionState, int packetId, ByteBuffer body) { - BinaryReader binaryReader = new BinaryReader(body); - return switch (connectionState) { - case PLAY -> playHandler.create(packetId, binaryReader); - case LOGIN -> loginHandler.create(packetId, binaryReader); - case STATUS -> statusHandler.create(packetId, binaryReader); + NetworkBuffer networkBuffer = new NetworkBuffer(body); + BinaryReader reader = new BinaryReader(networkBuffer); + final ClientPacket clientPacket = switch (connectionState) { + case PLAY -> playHandler.create(packetId, reader); + case LOGIN -> loginHandler.create(packetId, reader); + case STATUS -> statusHandler.create(packetId, reader); case UNKNOWN -> { assert packetId == 0; - yield new HandshakePacket(binaryReader); + yield new HandshakePacket(reader); } }; + body.position(networkBuffer.readIndex()); + return clientPacket; } public ClientPacket process(@NotNull PlayerConnection connection, int packetId, ByteBuffer body) { diff --git a/src/main/java/net/minestom/server/network/packet/client/login/LoginPluginResponsePacket.java b/src/main/java/net/minestom/server/network/packet/client/login/LoginPluginResponsePacket.java index 7e94e76dd..652af3113 100644 --- a/src/main/java/net/minestom/server/network/packet/client/login/LoginPluginResponsePacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/login/LoginPluginResponsePacket.java @@ -5,6 +5,7 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.extras.velocity.VelocityProxy; import net.minestom.server.network.ConnectionManager; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.client.ClientPreplayPacket; import net.minestom.server.network.packet.server.login.LoginDisconnectPacket; import net.minestom.server.network.player.GameProfile; @@ -18,6 +19,10 @@ import org.jetbrains.annotations.Nullable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; + +import static net.minestom.server.network.NetworkBuffer.STRING; public record LoginPluginResponsePacket(int messageId, byte @Nullable [] data) implements ClientPreplayPacket { private final static ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager(); @@ -41,14 +46,20 @@ public record LoginPluginResponsePacket(int messageId, byte @Nullable [] data) i // Velocity if (VelocityProxy.isEnabled() && channel.equals(VelocityProxy.PLAYER_INFO_CHANNEL)) { if (data != null && data.length > 0) { - BinaryReader reader = new BinaryReader(data); - success = VelocityProxy.checkIntegrity(reader); + NetworkBuffer buffer = new NetworkBuffer(ByteBuffer.wrap(data)); + success = VelocityProxy.checkIntegrity(buffer); if (success) { // Get the real connection address - final InetAddress address = VelocityProxy.readAddress(reader); + final InetAddress address; + try { + address = InetAddress.getByName(buffer.read(STRING)); + } catch (UnknownHostException e) { + MinecraftServer.getExceptionManager().handleException(e); + return; + } final int port = ((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort(); socketAddress = new InetSocketAddress(address, port); - gameProfile = new GameProfile(reader); + gameProfile = new GameProfile(new BinaryReader(buffer)); } } } diff --git a/src/main/java/net/minestom/server/tag/Serializers.java b/src/main/java/net/minestom/server/tag/Serializers.java index 952a23d4c..59b469842 100644 --- a/src/main/java/net/minestom/server/tag/Serializers.java +++ b/src/main/java/net/minestom/server/tag/Serializers.java @@ -3,9 +3,9 @@ package net.minestom.server.tag; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minestom.server.item.ItemStack; -import net.minestom.server.utils.Utils; import org.jglrxavpok.hephaistos.nbt.*; +import java.util.UUID; import java.util.function.Function; /** @@ -22,8 +22,8 @@ final class Serializers { static final Entry STRING = new Entry<>(NBTType.TAG_String, NBTString::getValue, NBT::String); static final Entry NBT_ENTRY = new Entry<>(null, Function.identity(), Function.identity()); - static final Entry UUID = new Entry<>(NBTType.TAG_Int_Array, intArray -> Utils.intArrayToUuid(intArray.getValue().copyArray()), - uuid -> NBT.IntArray(Utils.uuidToIntArray(uuid))); + static final Entry UUID = new Entry<>(NBTType.TAG_Int_Array, intArray -> intArrayToUuid(intArray.getValue().copyArray()), + uuid -> NBT.IntArray(uuidToIntArray(uuid))); static final Entry ITEM = new Entry<>(NBTType.TAG_Compound, ItemStack::fromItemNBT, ItemStack::toItemNBT); static final Entry COMPONENT = new Entry<>(NBTType.TAG_String, input -> GsonComponentSerializer.gson().deserialize(input.getValue()), component -> NBT.String(GsonComponentSerializer.gson().serialize(component))); @@ -55,4 +55,26 @@ final class Serializers { return writer.apply(value); } } + + private static int[] uuidToIntArray(UUID uuid) { + int[] array = new int[4]; + + final long uuidMost = uuid.getMostSignificantBits(); + final long uuidLeast = uuid.getLeastSignificantBits(); + + array[0] = (int) (uuidMost >> 32); + array[1] = (int) uuidMost; + + array[2] = (int) (uuidLeast >> 32); + array[3] = (int) uuidLeast; + + return array; + } + + private static UUID intArrayToUuid(int[] array) { + final long uuidMost = (long) array[0] << 32 | array[1] & 0xFFFFFFFFL; + final long uuidLeast = (long) array[2] << 32 | array[3] & 0xFFFFFFFFL; + + return new UUID(uuidMost, uuidLeast); + } } diff --git a/src/main/java/net/minestom/server/utils/NBTUtils.java b/src/main/java/net/minestom/server/utils/NBTUtils.java deleted file mode 100644 index eec170a12..000000000 --- a/src/main/java/net/minestom/server/utils/NBTUtils.java +++ /dev/null @@ -1,116 +0,0 @@ -package net.minestom.server.utils; - -import net.kyori.adventure.nbt.api.BinaryTagHolder; -import net.kyori.adventure.util.Codec; -import net.minestom.server.adventure.MinestomAdventure; -import net.minestom.server.inventory.Inventory; -import net.minestom.server.item.Enchantment; -import net.minestom.server.item.ItemStack; -import net.minestom.server.item.Material; -import net.minestom.server.utils.binary.BinaryReader; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jglrxavpok.hephaistos.nbt.*; -import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound; - -import java.util.List; -import java.util.Map; - -// for lack of a better name -@ApiStatus.Internal -public final class NBTUtils { - - /** - * An Adventure codec to convert between NBT and SNBT. - * - * @deprecated Use {@link MinestomAdventure#NBT_CODEC} - */ - @Deprecated(forRemoval = true) - public static final Codec SNBT_CODEC = MinestomAdventure.NBT_CODEC; - - private NBTUtils() { - } - - /** - * Turns an {@link NBTCompound} into an Adventure {@link BinaryTagHolder}. - * - * @param tag the tag, if any - * @return the binary tag holder, or {@code null} if the tag was null - */ - @Contract("null -> null; !null -> !null") - public static BinaryTagHolder asBinaryTagHolder(@Nullable NBTCompound tag) { - if (tag == null) { - return null; - } - - return BinaryTagHolder.encode(tag, MinestomAdventure.NBT_CODEC); - } - - /** - * Loads all the items from the 'items' list into the given inventory - * - * @param items the items to save - * @param destination the inventory destination - */ - public static void loadAllItems(@NotNull NBTList items, @NotNull Inventory destination) { - destination.clear(); - for (NBTCompound tag : items) { - Material material = Material.fromNamespaceId(tag.getString("id")); - if (material == Material.AIR) { - material = Material.STONE; - } - byte count = tag.getByte("Count"); - NBTCompound nbtCompound = null; - if (tag.containsKey("tag")) { - nbtCompound = tag.getCompound("tag"); - } - ItemStack itemStack = ItemStack.fromNBT(material, nbtCompound, count); - destination.setItemStack(tag.getByte("Slot"), itemStack); - } - } - - public static void saveAllItems(@NotNull List list, @NotNull Inventory inventory) { - for (int i = 0; i < inventory.getSize(); i++) { - final ItemStack stack = inventory.getItemStack(i); - final NBTCompound tag = stack.meta().toNBT(); - final int slotIndex = i; - list.add(NBT.Compound(nbt -> { - nbt.set("tag", tag); - nbt.setByte("Slot", (byte) slotIndex); - nbt.setByte("Count", (byte) stack.amount()); - nbt.setString("id", stack.material().name()); - })); - } - } - - public static void writeEnchant(@NotNull MutableNBTCompound nbt, @NotNull String listName, - @NotNull Map enchantmentMap) { - nbt.set(listName, NBT.List( - NBTType.TAG_Compound, - enchantmentMap.entrySet().stream() - .map(entry -> NBT.Compound(Map.of( - "lvl", NBT.Short(entry.getValue()), - "id", NBT.String(entry.getKey().name())))) - .toList() - )); - } - - public static @NotNull ItemStack readItemStack(@NotNull BinaryReader reader) { - final boolean present = reader.readBoolean(); - if (!present) return ItemStack.AIR; - - final int id = reader.readVarInt(); - if (id == -1) { - // Drop mode - return ItemStack.AIR; - } - - final Material material = Material.fromId((short) id); - final byte count = reader.readByte(); - NBTCompound nbtCompound = reader.readTag() instanceof NBTCompound compound ? - compound : null; - return ItemStack.fromNBT(material, nbtCompound, count); - } -} diff --git a/src/main/java/net/minestom/server/utils/PacketUtils.java b/src/main/java/net/minestom/server/utils/PacketUtils.java index eb65f8a5e..8f3772dbd 100644 --- a/src/main/java/net/minestom/server/utils/PacketUtils.java +++ b/src/main/java/net/minestom/server/utils/PacketUtils.java @@ -17,6 +17,7 @@ import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.server.*; import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.network.player.PlayerSocketConnection; @@ -255,24 +256,26 @@ public final class PacketUtils { int id, @NotNull Writeable writeable, int compressionThreshold) { - BinaryWriter writerView = BinaryWriter.view(buffer); // ensure that the buffer is not resized + NetworkBuffer networkBuffer = new NetworkBuffer(buffer, false); + BinaryWriter legacyWriter = new BinaryWriter(networkBuffer); if (compressionThreshold <= 0) { // Uncompressed format https://wiki.vg/Protocol#Without_compression - final int lengthIndex = Utils.writeEmptyVarIntHeader(buffer); - Utils.writeVarInt(buffer, id); - writeable.write(writerView); - final int finalSize = buffer.position() - (lengthIndex + 3); + final int lengthIndex = networkBuffer.skipWrite(3); + networkBuffer.write(NetworkBuffer.VAR_INT, id); + writeable.write(legacyWriter); + final int finalSize = networkBuffer.writeIndex() - (lengthIndex + 3); Utils.writeVarIntHeader(buffer, lengthIndex, finalSize); + buffer.position(networkBuffer.writeIndex()); return; } // Compressed format https://wiki.vg/Protocol#With_compression - final int compressedIndex = Utils.writeEmptyVarIntHeader(buffer); - final int uncompressedIndex = Utils.writeEmptyVarIntHeader(buffer); + final int compressedIndex = networkBuffer.skipWrite(3); + final int uncompressedIndex = networkBuffer.skipWrite(3); - final int contentStart = buffer.position(); - Utils.writeVarInt(buffer, id); - writeable.write(writerView); - final int packetSize = buffer.position() - contentStart; + final int contentStart = networkBuffer.writeIndex(); + networkBuffer.write(NetworkBuffer.VAR_INT, id); + writeable.write(legacyWriter); + final int packetSize = networkBuffer.writeIndex() - contentStart; final boolean compressed = packetSize >= compressionThreshold; if (compressed) { // Packet large enough, compress it @@ -283,11 +286,15 @@ public final class PacketUtils { deflater.finish(); deflater.deflate(buffer.position(contentStart)); deflater.reset(); + + networkBuffer.skipWrite(buffer.position() - contentStart); } } // Packet header (Packet + Data Length) - Utils.writeVarIntHeader(buffer, compressedIndex, buffer.position() - uncompressedIndex); + Utils.writeVarIntHeader(buffer, compressedIndex, networkBuffer.writeIndex() - uncompressedIndex); Utils.writeVarIntHeader(buffer, uncompressedIndex, compressed ? packetSize : 0); + + buffer.position(networkBuffer.writeIndex()); } @ApiStatus.Internal diff --git a/src/main/java/net/minestom/server/utils/SerializerUtils.java b/src/main/java/net/minestom/server/utils/SerializerUtils.java deleted file mode 100644 index d0e81a250..000000000 --- a/src/main/java/net/minestom/server/utils/SerializerUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.minestom.server.utils; - -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Vec; - -public final class SerializerUtils { - - private SerializerUtils() { - - } - - public static long positionToLong(int x, int y, int z) { - return (((long) x & 0x3FFFFFF) << 38) | (((long) z & 0x3FFFFFF) << 12) | ((long) y & 0xFFF); - } - - public static Point longToBlockPosition(long value) { - final int x = (int) (value >> 38); - final int y = (int) (value << 52 >> 52); - final int z = (int) (value << 26 >> 38); - return new Vec(x, y, z); - } -} diff --git a/src/main/java/net/minestom/server/utils/Utils.java b/src/main/java/net/minestom/server/utils/Utils.java index f31e25368..972212cba 100644 --- a/src/main/java/net/minestom/server/utils/Utils.java +++ b/src/main/java/net/minestom/server/utils/Utils.java @@ -45,12 +45,6 @@ public final class Utils { buffer.put(startIndex + 2, (byte) (value >>> 14)); } - public static int writeEmptyVarIntHeader(@NotNull ByteBuffer buffer) { - final int index = buffer.position(); - buffer.position(index + 3); // Skip 3 bytes - return index; - } - public static int readVarInt(ByteBuffer buf) { // https://github.com/jvm-profiling-tools/async-profiler/blob/a38a375dc62b31a8109f3af97366a307abb0fe6f/src/converter/one/jfr/JfrReader.java#L393 int result = 0; @@ -62,94 +56,4 @@ public final class Utils { } } } - - public static long readVarLong(@NotNull ByteBuffer buf) { - // https://github.com/jvm-profiling-tools/async-profiler/blob/a38a375dc62b31a8109f3af97366a307abb0fe6f/src/converter/one/jfr/JfrReader.java#L404 - long result = 0; - for (int shift = 0; shift < 56; shift += 7) { - byte b = buf.get(); - result |= (b & 0x7fL) << shift; - if (b >= 0) { - return result; - } - } - return result | (buf.get() & 0xffL) << 56; - } - - public static void writeVarLong(ByteBuffer buffer, long value) { - do { - byte temp = (byte) (value & 0b01111111); - value >>>= 7; - if (value != 0) { - temp |= 0b10000000; - } - buffer.put(temp); - } while (value != 0); - } - - public static int[] uuidToIntArray(UUID uuid) { - int[] array = new int[4]; - - final long uuidMost = uuid.getMostSignificantBits(); - final long uuidLeast = uuid.getLeastSignificantBits(); - - array[0] = (int) (uuidMost >> 32); - array[1] = (int) uuidMost; - - array[2] = (int) (uuidLeast >> 32); - array[3] = (int) uuidLeast; - - return array; - } - - public static UUID intArrayToUuid(int[] array) { - final long uuidMost = (long) array[0] << 32 | array[1] & 0xFFFFFFFFL; - final long uuidLeast = (long) array[2] << 32 | array[3] & 0xFFFFFFFFL; - - return new UUID(uuidMost, uuidLeast); - } - - private static final int[] MAGIC = { - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - public static long[] encodeBlocks(int[] blocks, int bitsPerEntry) { - final long maxEntryValue = (1L << bitsPerEntry) - 1; - final char valuesPerLong = (char) (64 / bitsPerEntry); - final int magicIndex = 3 * (valuesPerLong - 1); - final long divideMul = Integer.toUnsignedLong(MAGIC[magicIndex]); - final long divideAdd = Integer.toUnsignedLong(MAGIC[magicIndex + 1]); - final int divideShift = MAGIC[magicIndex + 2]; - final int size = (blocks.length + valuesPerLong - 1) / valuesPerLong; - - long[] data = new long[size]; - - for (int i = 0; i < blocks.length; i++) { - final long value = blocks[i]; - final int cellIndex = (int) (i * divideMul + divideAdd >> 32L >> divideShift); - final int bitIndex = (i - cellIndex * valuesPerLong) * bitsPerEntry; - data[cellIndex] = data[cellIndex] & ~(maxEntryValue << bitIndex) | (value & maxEntryValue) << bitIndex; - } - - return data; - } - } diff --git a/src/main/java/net/minestom/server/utils/binary/BinaryReader.java b/src/main/java/net/minestom/server/utils/binary/BinaryReader.java index c804bdf36..a3cf3b520 100644 --- a/src/main/java/net/minestom/server/utils/binary/BinaryReader.java +++ b/src/main/java/net/minestom/server/utils/binary/BinaryReader.java @@ -2,21 +2,14 @@ package net.minestom.server.utils.binary; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.item.ItemStack; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.Either; -import net.minestom.server.utils.NBTUtils; -import net.minestom.server.utils.SerializerUtils; -import net.minestom.server.utils.Utils; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; -import org.jglrxavpok.hephaistos.nbt.CompressedProcesser; import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.NBTException; -import org.jglrxavpok.hephaistos.nbt.NBTReader; -import java.io.IOException; import java.io.InputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; @@ -27,17 +20,22 @@ import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; +import static net.minestom.server.network.NetworkBuffer.*; + /** * Class used to read from a byte array. *

* WARNING: not thread-safe. */ public class BinaryReader extends InputStream { - private final ByteBuffer buffer; - private NBTReader nbtReader = null; + private final NetworkBuffer buffer; + + public BinaryReader(NetworkBuffer buffer) { + this.buffer = buffer; + } public BinaryReader(@NotNull ByteBuffer buffer) { - this.buffer = buffer; + this.buffer = new NetworkBuffer(buffer); } public BinaryReader(byte[] bytes) { @@ -45,57 +43,53 @@ public class BinaryReader extends InputStream { } public int readVarInt() { - return Utils.readVarInt(buffer); + return buffer.read(VAR_INT); } public long readVarLong() { - return Utils.readVarLong(buffer); + return buffer.read(VAR_LONG); } public boolean readBoolean() { - return buffer.get() == 1; + return buffer.read(BOOLEAN); } public byte readByte() { - return buffer.get(); + return buffer.read(BYTE); } public short readShort() { - return buffer.getShort(); - } - - public char readChar() { - return buffer.getChar(); + return buffer.read(SHORT); } public int readUnsignedShort() { - return buffer.getShort() & 0xFFFF; + return buffer.read(SHORT) & 0xFFFF; } /** * Same as readInt */ public int readInteger() { - return buffer.getInt(); + return buffer.read(INT); } /** * Same as readInteger, created for parity with BinaryWriter */ public int readInt() { - return buffer.getInt(); + return buffer.read(INT); } public long readLong() { - return buffer.getLong(); + return buffer.read(LONG); } public float readFloat() { - return buffer.getFloat(); + return buffer.read(FLOAT); } public double readDouble() { - return buffer.getDouble(); + return buffer.read(DOUBLE); } /** @@ -112,9 +106,11 @@ public class BinaryReader extends InputStream { final int length = readVarInt(); byte[] bytes = new byte[length]; try { - this.buffer.get(bytes); + for (int i = 0; i < length; i++) { + bytes[i] = readByte(); + } } catch (BufferUnderflowException e) { - throw new RuntimeException("Could not read " + length + ", " + buffer.remaining() + " remaining."); + throw new RuntimeException("Could not read " + length + ", " + buffer.readableBytes() + " remaining."); } final String str = new String(bytes, StandardCharsets.UTF_8); Check.stateCondition(str.length() > maxLength, @@ -123,12 +119,14 @@ public class BinaryReader extends InputStream { } public String readSizedString() { - return readSizedString(Integer.MAX_VALUE); + return buffer.read(STRING); } public byte[] readBytes(int length) { byte[] bytes = new byte[length]; - buffer.get(bytes); + for (int i = 0; i < length; i++) { + bytes[i] = readByte(); + } return bytes; } @@ -177,27 +175,19 @@ public class BinaryReader extends InputStream { } public byte[] readRemainingBytes() { - return readBytes(available()); + return buffer.read(RAW_BYTES); } public Point readBlockPosition() { - return SerializerUtils.longToBlockPosition(buffer.getLong()); + return buffer.read(BLOCK_POSITION); } public UUID readUuid() { - return new UUID(readLong(), readLong()); + return buffer.read(UUID); } - /** - * Tries to read an {@link ItemStack}. - * - * @return the read item - * @throws NullPointerException if the item could not get read - */ public ItemStack readItemStack() { - final ItemStack itemStack = NBTUtils.readItemStack(this); - Check.notNull(itemStack, "#readSlot returned null, probably because the buffer was corrupted"); - return itemStack; + return buffer.read(ITEM); } public Component readComponent(int maxLength) { @@ -206,7 +196,7 @@ public class BinaryReader extends InputStream { } public Component readComponent() { - return readComponent(Integer.MAX_VALUE); + return buffer.read(COMPONENT); } /** @@ -263,10 +253,6 @@ public class BinaryReader extends InputStream { return list; } - public ByteBuffer getBuffer() { - return buffer; - } - @Override public int read() { return readByte() & 0xFF; @@ -274,21 +260,11 @@ public class BinaryReader extends InputStream { @Override public int available() { - return buffer.remaining(); + return buffer.readableBytes(); } public NBT readTag() { - NBTReader reader = this.nbtReader; - if (reader == null) { - reader = new NBTReader(this, CompressedProcesser.NONE); - this.nbtReader = reader; - } - try { - return reader.read(); - } catch (IOException | NBTException e) { - MinecraftServer.getExceptionManager().handleException(e); - throw new RuntimeException(); - } + return buffer.read(NBT); } /** @@ -299,11 +275,12 @@ public class BinaryReader extends InputStream { * @param extractor the extraction code, simply call the reader's read* methods here. */ public byte[] extractBytes(Runnable extractor) { - int startingPosition = buffer.position(); + int startingPosition = buffer.readIndex(); extractor.run(); - int endingPosition = getBuffer().position(); + int endingPosition = buffer.readIndex(); byte[] output = new byte[endingPosition - startingPosition]; - buffer.get(startingPosition, output); + buffer.copyTo(buffer.readIndex(), output, 0, output.length); + //buffer.get(startingPosition, output); return output; } } diff --git a/src/main/java/net/minestom/server/utils/binary/BinaryWriter.java b/src/main/java/net/minestom/server/utils/binary/BinaryWriter.java index 93f475eaa..54b417cd2 100644 --- a/src/main/java/net/minestom/server/utils/binary/BinaryWriter.java +++ b/src/main/java/net/minestom/server/utils/binary/BinaryWriter.java @@ -1,49 +1,42 @@ package net.minestom.server.utils.binary; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; import net.minestom.server.item.ItemStack; +import net.minestom.server.network.NetworkBuffer; import net.minestom.server.utils.Either; -import net.minestom.server.utils.SerializerUtils; -import net.minestom.server.utils.Utils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.jglrxavpok.hephaistos.nbt.CompressedProcesser; import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.NBTWriter; -import java.io.IOException; import java.io.OutputStream; -import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.Collection; import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static net.minestom.server.network.NetworkBuffer.*; + /** * Class used to write to a byte array. * WARNING: not thread-safe. */ public class BinaryWriter extends OutputStream { - private ByteBuffer buffer; - private NBTWriter nbtWriter; // Lazily initialized + private final NetworkBuffer buffer; - private final boolean resizable; + public BinaryWriter(NetworkBuffer buffer) { + this.buffer = buffer; + } private BinaryWriter(ByteBuffer buffer, boolean resizable) { - this.buffer = buffer; - this.resizable = resizable; + this.buffer = new NetworkBuffer(buffer, resizable); } public BinaryWriter(@NotNull ByteBuffer buffer) { - this.buffer = buffer; - this.resizable = true; + this.buffer = new NetworkBuffer(buffer); } public BinaryWriter(int initialCapacity) { @@ -59,158 +52,55 @@ public class BinaryWriter extends OutputStream { return new BinaryWriter(buffer, false); } - protected void ensureSize(int length) { - if (!resizable) return; - final int position = buffer.position(); - if (position + length >= buffer.limit()) { - final int newLength = (position + length) * 4; - var copy = buffer.isDirect() ? - ByteBuffer.allocateDirect(newLength) : ByteBuffer.allocate(newLength); - copy.put(buffer.flip()); - this.buffer = copy; - } - } - - /** - * Writes a component to the buffer as a sized string. - * - * @param component the component - */ public void writeComponent(@NotNull Component component) { - this.writeSizedString(GsonComponentSerializer.gson().serialize(component)); + this.buffer.write(COMPONENT, component); } - /** - * Writes a single byte to the buffer. - * - * @param b the byte to write - */ public void writeByte(byte b) { - ensureSize(Byte.BYTES); - buffer.put(b); + this.buffer.write(BYTE, b); } - /** - * Writes a single boolean to the buffer. - * - * @param b the boolean to write - */ public void writeBoolean(boolean b) { - writeByte((byte) (b ? 1 : 0)); + this.buffer.write(BOOLEAN, b); } - /** - * Writes a single char to the buffer. - * - * @param c the char to write - */ - public void writeChar(char c) { - ensureSize(Character.BYTES); - buffer.putChar(c); - } - - /** - * Writes a single short to the buffer. - * - * @param s the short to write - */ public void writeShort(short s) { - ensureSize(Short.BYTES); - buffer.putShort(s); + this.buffer.write(SHORT, s); } - /** - * Writes a single int to the buffer. - * - * @param i the int to write - */ public void writeInt(int i) { - ensureSize(Integer.BYTES); - buffer.putInt(i); + this.buffer.write(INT, i); } - /** - * Writes a single long to the buffer. - * - * @param l the long to write - */ public void writeLong(long l) { - ensureSize(Long.BYTES); - buffer.putLong(l); + this.buffer.write(LONG, l); } - /** - * Writes a single float to the buffer. - * - * @param f the float to write - */ public void writeFloat(float f) { - ensureSize(Float.BYTES); - buffer.putFloat(f); + this.buffer.write(FLOAT, f); } - /** - * Writes a single double to the buffer. - * - * @param d the double to write - */ public void writeDouble(double d) { - ensureSize(Double.BYTES); - buffer.putDouble(d); + this.buffer.write(DOUBLE, d); } - /** - * Writes a single var-int to the buffer. - * - * @param i the int to write - */ public void writeVarInt(int i) { - ensureSize(5); - Utils.writeVarInt(buffer, i); + this.buffer.write(VAR_INT, i); } - /** - * Writes a single var-long to the buffer. - * - * @param l the long to write - */ public void writeVarLong(long l) { - ensureSize(10); - Utils.writeVarLong(buffer, l); + this.buffer.write(VAR_LONG, l); } - /** - * Writes a string to the buffer. - *

- * The size is a var-int type. - * - * @param string the string to write - */ public void writeSizedString(@NotNull String string) { - final var bytes = string.getBytes(StandardCharsets.UTF_8); - writeVarInt(bytes.length); - writeBytes(bytes); + this.buffer.write(STRING, string); } - /** - * Writes a null terminated string to the buffer. This method adds the null character - * to the end of the string before writing. - * - * @param string the string to write - * @param charset the charset to encode in - */ public void writeNullTerminatedString(@NotNull String string, @NotNull Charset charset) { final var bytes = (string + '\0').getBytes(charset); writeBytes(bytes); } - /** - * Writes a var-int array to the buffer. - *

- * It is sized by another var-int at the beginning. - * - * @param array the integers to write - */ public void writeVarIntArray(int[] array) { if (array == null) { writeVarInt(0); @@ -253,46 +143,16 @@ public class BinaryWriter extends OutputStream { writeBytes(array); } - /** - * Writes a byte array. - *

- * WARNING: it doesn't write the length of {@code bytes}. - * - * @param bytes the byte array to write - */ public void writeBytes(byte @NotNull [] bytes) { - if (bytes.length == 0) return; - ensureSize(bytes.length); - buffer.put(bytes); + this.buffer.write(RAW_BYTES, bytes); } - /** - * Writes a string to the buffer. - *

- * The array is sized by a var-int and all strings are wrote using {@link #writeSizedString(String)}. - * - * @param array the string array to write - */ public void writeStringArray(@NotNull String[] array) { - if (array == null) { - writeVarInt(0); - return; - } - writeVarInt(array.length); - for (String element : array) { - writeSizedString(element); - } + this.buffer.writeCollection(STRING, array); } - /** - * Writes an {@link UUID}. - * It is done by writing both long, the most and least significant bits. - * - * @param uuid the {@link UUID} to write - */ public void writeUuid(@NotNull UUID uuid) { - writeLong(uuid.getMostSignificantBits()); - writeLong(uuid.getLeastSignificantBits()); + this.buffer.write(UUID, uuid); } public void writeBlockPosition(@NotNull Point point) { @@ -300,30 +160,15 @@ public class BinaryWriter extends OutputStream { } public void writeBlockPosition(int x, int y, int z) { - writeLong(SerializerUtils.positionToLong(x, y, z)); + this.buffer.write(BLOCK_POSITION, new Vec(x, y, z)); } public void writeItemStack(@NotNull ItemStack itemStack) { - if (itemStack.isAir()) { - writeBoolean(false); - } else { - writeBoolean(true); - writeVarInt(itemStack.material().id()); - writeByte((byte) itemStack.amount()); - write(itemStack.meta()); - } + this.buffer.write(ITEM, itemStack); } public void writeNBT(@NotNull String name, @NotNull NBT tag) { - if (nbtWriter == null) { - this.nbtWriter = new NBTWriter(this, CompressedProcesser.NONE); - } - try { - nbtWriter.writeNamed(name, tag); - } catch (IOException e) { - // should not throw, as nbtWriter points to this PacketWriter - MinecraftServer.getExceptionManager().handleException(e); - } + this.buffer.write(NBT, tag); } public void writeEither(Either either, BiConsumer leftWriter, BiConsumer rightWriter) { @@ -346,12 +191,13 @@ public class BinaryWriter extends OutputStream { } public void write(@NotNull ByteBuffer buffer) { - ensureSize(buffer.remaining()); - this.buffer.put(buffer); + byte[] remaining = new byte[buffer.remaining()]; + buffer.get(remaining); + writeBytes(remaining); } public void write(@NotNull BinaryWriter writer) { - write(writer.buffer); + writeBytes(writer.toByteArray()); } /** @@ -387,73 +233,18 @@ public class BinaryWriter extends OutputStream { * @return the byte array containing all the {@link BinaryWriter} data */ public byte[] toByteArray() { - buffer.flip(); - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); + byte[] bytes = new byte[buffer.writeIndex()]; + this.buffer.copyTo(0, bytes, 0, bytes.length); return bytes; } - /** - * Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the beginning of this writer. - * - * @param headerWriter the {@link BinaryWriter} to add at the beginning - */ - public void writeAtStart(@NotNull BinaryWriter headerWriter) { - // Get the buffer of the header - final var headerBuf = headerWriter.getBuffer(); - // Merge both the headerBuf and this buffer - final var finalBuffer = concat(headerBuf, buffer); - // Change the buffer used by this writer - setBuffer(finalBuffer); - } - - /** - * Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the end of this writer. - * - * @param footerWriter the {@link BinaryWriter} to add at the end - */ - public void writeAtEnd(@NotNull BinaryWriter footerWriter) { - // Get the buffer of the footer - final var footerBuf = footerWriter.getBuffer(); - // Merge both this buffer and the footerBuf - final var finalBuffer = concat(buffer, footerBuf); - // Change the buffer used by this writer - setBuffer(finalBuffer); - } - - public static ByteBuffer concat(final ByteBuffer... buffers) { - final ByteBuffer combined = ByteBuffer.allocate(Arrays.stream(buffers).mapToInt(Buffer::remaining).sum()); - Arrays.stream(buffers).forEach(b -> combined.put(b.duplicate())); - return combined; - } - - /** - * Gets the raw buffer used by this binary writer. - * - * @return the raw buffer - */ - public @NotNull ByteBuffer getBuffer() { - return buffer; - } - - /** - * Changes the buffer used by this binary writer. - * - * @param buffer the new buffer used by this binary writer - */ - public void setBuffer(ByteBuffer buffer) { - this.buffer = buffer; - } - @Override public void write(int b) { writeByte((byte) b); } public void writeUnsignedShort(int yourShort) { - // FIXME unsigned - ensureSize(Short.BYTES); - buffer.putShort((short) (yourShort & 0xFFFF)); + this.buffer.write(SHORT, (short) (yourShort & 0xFFFF)); } /** diff --git a/src/test/java/net/minestom/server/network/NetworkBufferTest.java b/src/test/java/net/minestom/server/network/NetworkBufferTest.java new file mode 100644 index 000000000..13126fc4b --- /dev/null +++ b/src/test/java/net/minestom/server/network/NetworkBufferTest.java @@ -0,0 +1,286 @@ +package net.minestom.server.network; + +import net.kyori.adventure.text.Component; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NetworkBufferTest { + + @Test + public void readableBytes() { + var buffer = new NetworkBuffer(); + assertEquals(0, buffer.readableBytes()); + + buffer.write(NetworkBuffer.BYTE, (byte) 0); + assertEquals(1, buffer.readableBytes()); + + buffer.write(NetworkBuffer.LONG, 50L); + assertEquals(9, buffer.readableBytes()); + + assertEquals((byte) 0, buffer.read(NetworkBuffer.BYTE)); + assertEquals(8, buffer.readableBytes()); + + assertEquals(50L, buffer.read(NetworkBuffer.LONG)); + assertEquals(0, buffer.readableBytes()); + } + + @Test + public void numbers() { + assertBufferType(NetworkBuffer.BOOLEAN, false, new byte[]{0x00}); + assertBufferType(NetworkBuffer.BOOLEAN, true, new byte[]{0x01}); + + assertBufferType(NetworkBuffer.BYTE, (byte) 0x00, new byte[]{0x00}); + assertBufferType(NetworkBuffer.BYTE, (byte) 0x01, new byte[]{0x01}); + assertBufferType(NetworkBuffer.BYTE, (byte) 0x7F, new byte[]{0x7F}); + assertBufferType(NetworkBuffer.BYTE, (byte) 0x80, new byte[]{(byte) 0x80}); + assertBufferType(NetworkBuffer.BYTE, (byte) 0xFF, new byte[]{(byte) 0xFF}); + + assertBufferType(NetworkBuffer.SHORT, (short) 0x0000, new byte[]{0x00, 0x00}); + assertBufferType(NetworkBuffer.SHORT, (short) 0x0001, new byte[]{0x00, 0x01}); + assertBufferType(NetworkBuffer.SHORT, (short) 0x7FFF, new byte[]{0x7F, (byte) 0xFF}); + assertBufferType(NetworkBuffer.SHORT, (short) 0x8000, new byte[]{(byte) 0x80, 0x00}); + assertBufferType(NetworkBuffer.SHORT, (short) 0xFFFF, new byte[]{(byte) 0xFF, (byte) 0xFF}); + + assertBufferType(NetworkBuffer.INT, 0, new byte[]{0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.INT, 1, new byte[]{0x00, 0x00, 0x00, 0x01}); + assertBufferType(NetworkBuffer.INT, 2, new byte[]{0x00, 0x00, 0x00, 0x02}); + assertBufferType(NetworkBuffer.INT, 127, new byte[]{0x00, 0x00, 0x00, 0x7F}); + assertBufferType(NetworkBuffer.INT, 128, new byte[]{0x00, 0x00, 0x00, (byte) 0x80}); + assertBufferType(NetworkBuffer.INT, 255, new byte[]{0x00, 0x00, 0x00, (byte) 0xFF}); + assertBufferType(NetworkBuffer.INT, 256, new byte[]{0x00, 0x00, 0x01, 0x00}); + assertBufferType(NetworkBuffer.INT, 25565, new byte[]{0x00, 0x00, 0x63, (byte) 0xDD}); + assertBufferType(NetworkBuffer.INT, 32767, new byte[]{0x00, 0x00, 0x7F, (byte) 0xFF}); + assertBufferType(NetworkBuffer.INT, 32768, new byte[]{0x00, 0x00, (byte) 0x80, 0x00}); + assertBufferType(NetworkBuffer.INT, 65535, new byte[]{0x00, 0x00, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.INT, 65536, new byte[]{0x00, 0x01, 0x00, 0x00}); + assertBufferType(NetworkBuffer.INT, 2147483647, new byte[]{0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.INT, -1, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.INT, -2147483648, new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}); + + assertBufferType(NetworkBuffer.LONG, 0L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, 1L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}); + assertBufferType(NetworkBuffer.LONG, 2L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}); + assertBufferType(NetworkBuffer.LONG, 127L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F}); + assertBufferType(NetworkBuffer.LONG, 128L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80}); + assertBufferType(NetworkBuffer.LONG, 255L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, 256L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}); + assertBufferType(NetworkBuffer.LONG, 25565L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, (byte) 0xDD}); + assertBufferType(NetworkBuffer.LONG, 32767L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, 32768L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00}); + assertBufferType(NetworkBuffer.LONG, 65535L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, 65536L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, 2147483647L, new byte[]{0x00, 0x00, 0x00, 0x00, 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, 2147483648L, new byte[]{0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, 4294967295L, new byte[]{0x00, 0x00, 0x00, 0x00, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, 4294967296L, new byte[]{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, 9223372036854775807L, new byte[]{0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, -1L, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); + assertBufferType(NetworkBuffer.LONG, -2147483648L, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x80, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, -4294967296L, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.LONG, -9223372036854775808L, new byte[]{(byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + + assertBufferType(NetworkBuffer.FLOAT, 0f, new byte[]{0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 1f, new byte[]{0x3F, (byte) 0x80, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 1.1f, new byte[]{0x3F, (byte) 0x8C, (byte) 0xCC, (byte) 0xCD}); + assertBufferType(NetworkBuffer.FLOAT, 1.5f, new byte[]{0x3F, (byte) 0xC0, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 1.6f, new byte[]{0x3F, (byte) 0xCC, (byte) 0xCC, (byte) 0xCD}); + assertBufferType(NetworkBuffer.FLOAT, 2f, new byte[]{0x40, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 2.5f, new byte[]{0x40, 0x20, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 3f, new byte[]{0x40, 0x40, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 4f, new byte[]{0x40, (byte) 0x80, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 5f, new byte[]{0x40, (byte) 0xA0, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 10f, new byte[]{0x41, 0x20, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 100f, new byte[]{0x42, (byte) 0xC8, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 1000f, new byte[]{0x44, 0x7a, 0x00, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 10000f, new byte[]{0x46, 0x1C, 0x40, 0x00}); + assertBufferType(NetworkBuffer.FLOAT, 100000f, new byte[]{0x47, (byte) 0xC3, 0x50, 0x00}); + + assertBufferType(NetworkBuffer.DOUBLE, 0d, new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 1d, new byte[]{0x3F, (byte) 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 1.1d, new byte[]{0x3F, (byte) 0xF1, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x9A}); + assertBufferType(NetworkBuffer.DOUBLE, 1.5d, new byte[]{0x3F, (byte) 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 1.6d, new byte[]{0x3F, (byte) 0xF9, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x99, (byte) 0x9A}); + assertBufferType(NetworkBuffer.DOUBLE, 2d, new byte[]{0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 2.5d, new byte[]{0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 3d, new byte[]{0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 4d, new byte[]{0x40, 0x10, (byte) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 5d, new byte[]{0x40, 0x14, (byte) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 10d, new byte[]{0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 100d, new byte[]{0x40, 0x59, (byte) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 1000d, new byte[]{0x40, (byte) 0x8F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00}); + assertBufferType(NetworkBuffer.DOUBLE, 10000d, new byte[]{0x40, (byte) 0xC3, (byte) 0x88, 0x00, 0x00, 0x00, 0x00, 0x00}); + } + + @Test + public void varInt() { + assertBufferType(NetworkBuffer.VAR_INT, 0, new byte[]{0}); + assertBufferType(NetworkBuffer.VAR_INT, 1, new byte[]{0x01}); + assertBufferType(NetworkBuffer.VAR_INT, 2, new byte[]{0x02}); + assertBufferType(NetworkBuffer.VAR_INT, 11, new byte[]{0x0B}); + assertBufferType(NetworkBuffer.VAR_INT, 127, new byte[]{0x7f}); + assertBufferType(NetworkBuffer.VAR_INT, 128, new byte[]{(byte) 0x80, 0x01}); + assertBufferType(NetworkBuffer.VAR_INT, 255, new byte[]{(byte) 0xff, 0x01}); + assertBufferType(NetworkBuffer.VAR_INT, 25565, new byte[]{(byte) 0xdd, (byte) 0xc7, 0x01}); + assertBufferType(NetworkBuffer.VAR_INT, 2097151, new byte[]{(byte) 0xff, (byte) 0xff, 0x7f}); + assertBufferType(NetworkBuffer.VAR_INT, 2147483647, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x07}); + assertBufferType(NetworkBuffer.VAR_INT, -1, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x0f}); + assertBufferType(NetworkBuffer.VAR_INT, -2147483648, new byte[]{(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x08}); + } + + @Test + public void varLong() { + assertBufferType(NetworkBuffer.VAR_LONG, 0L, new byte[]{0}); + assertBufferType(NetworkBuffer.VAR_LONG, 1L, new byte[]{0x01}); + assertBufferType(NetworkBuffer.VAR_LONG, 2L, new byte[]{0x02}); + assertBufferType(NetworkBuffer.VAR_LONG, 127L, new byte[]{0x7f}); + assertBufferType(NetworkBuffer.VAR_LONG, 128L, new byte[]{(byte) 0x80, 0x01}); + assertBufferType(NetworkBuffer.VAR_LONG, 255L, new byte[]{(byte) 0xff, 0x01}); + assertBufferType(NetworkBuffer.VAR_LONG, 2147483647L, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x07}); + assertBufferType(NetworkBuffer.VAR_LONG, 9223372036854775807L, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x7f}); + assertBufferType(NetworkBuffer.VAR_LONG, -1L, new byte[]{(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x01}); + assertBufferType(NetworkBuffer.VAR_LONG, -2147483648L, new byte[]{(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x01}); + assertBufferType(NetworkBuffer.VAR_LONG, -9223372036854775808L, new byte[]{(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01}); + } + + @Test + public void rawBytes() { + // FIXME: currently break because the array is identity compared + //assertBufferType(NetworkBuffer.RAW_BYTES, new byte[]{0x0B, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64}, + // new byte[]{0x0B, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64}); + } + + @Test + public void string() { + assertBufferType(NetworkBuffer.STRING, "Hello World", new byte[]{0x0B, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64}); + } + + @Test + public void nbt() { + assertBufferType(NetworkBuffer.NBT, NBT.Int(5)); + assertBufferType(NetworkBuffer.NBT, NBT.Compound(Map.of("key", NBT.Int(5)))); + } + + @Test + public void component() { + assertBufferType(NetworkBuffer.COMPONENT, Component.text("Hello world")); + } + + @Test + public void uuid() { + assertBufferType(NetworkBuffer.UUID, new UUID(0, 0), new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + assertBufferType(NetworkBuffer.UUID, new UUID(1, 1), new byte[]{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1}); + } + + @Test + public void item() { + assertBufferType(NetworkBuffer.ITEM, ItemStack.AIR); + assertBufferType(NetworkBuffer.ITEM, ItemStack.of(Material.STONE, 1)); + assertBufferType(NetworkBuffer.ITEM, ItemStack.of(Material.DIAMOND_AXE, 1).withMeta(builder -> builder.damage(1))); + } + + @Test + public void optional() { + assertBufferTypeOptional(NetworkBuffer.BOOLEAN, null, new byte[]{0}); + assertBufferTypeOptional(NetworkBuffer.BOOLEAN, true, new byte[]{1, 1}); + } + + @Test + public void collection() { + assertBufferTypeCollection(NetworkBuffer.BOOLEAN, List.of(), new byte[]{0}); + assertBufferTypeCollection(NetworkBuffer.BOOLEAN, List.of(true), new byte[]{0x01, 0x01}); + } + + static void assertBufferType(NetworkBuffer.@NotNull Type type, @UnknownNullability T value, byte[] expected, @NotNull Action action) { + var buffer = new NetworkBuffer(); + action.write(buffer, type, value); + assertEquals(0, buffer.readIndex()); + if (expected != null) assertEquals(expected.length, buffer.writeIndex()); + + var actual = action.read(buffer, type); + + assertEquals(value, actual); + if (expected != null) assertEquals(expected.length, buffer.readIndex(), "Invalid read index"); + if (expected != null) assertEquals(expected.length, buffer.writeIndex()); + + if (expected != null) { + var bytes = new byte[expected.length]; + buffer.copyTo(0, bytes, 0, bytes.length); + assertArrayEquals(expected, bytes, "Invalid bytes: " + Arrays.toString(expected) + " != " + Arrays.toString(bytes)); + } + } + + static void assertBufferType(NetworkBuffer.@NotNull Type type, @NotNull T value, byte @Nullable [] expected) { + assertBufferType(type, value, expected, new Action<>() { + @Override + public void write(@NotNull NetworkBuffer buffer, @NotNull NetworkBuffer.Type type, @UnknownNullability T value) { + buffer.write(type, value); + } + + @Override + public T read(@NotNull NetworkBuffer buffer, @NotNull NetworkBuffer.Type type) { + return buffer.read(type); + } + }); + } + + static void assertBufferType(NetworkBuffer.@NotNull Type type, @NotNull T value) { + assertBufferType(type, value, null); + } + + static void assertBufferTypeOptional(NetworkBuffer.@NotNull Type type, @Nullable T value, byte @Nullable [] expected) { + assertBufferType(type, value, expected, new Action() { + @Override + public void write(@NotNull NetworkBuffer buffer, @NotNull NetworkBuffer.Type type, @UnknownNullability T value) { + buffer.writeOptional(type, value); + } + + @Override + public T read(@NotNull NetworkBuffer buffer, @NotNull NetworkBuffer.Type type) { + return buffer.readOptional(type); + } + }); + } + + static void assertBufferTypeOptional(NetworkBuffer.@NotNull Type type, @Nullable T value) { + assertBufferTypeOptional(type, value, null); + } + + static void assertBufferTypeCollection(NetworkBuffer.@NotNull Type type, @NotNull Collection values, byte @Nullable [] expected) { + var buffer = new NetworkBuffer(); + buffer.writeCollection(type, values); + assertEquals(0, buffer.readIndex()); + if (expected != null) assertEquals(expected.length, buffer.writeIndex()); + + var actual = buffer.readCollection(type); + + assertEquals(values, actual); + if (expected != null) assertEquals(expected.length, buffer.readIndex()); + if (expected != null) assertEquals(expected.length, buffer.writeIndex()); + + if (expected != null) { + var bytes = new byte[expected.length]; + buffer.copyTo(0, bytes, 0, bytes.length); + assertArrayEquals(expected, bytes, "Invalid bytes: " + Arrays.toString(expected) + " != " + Arrays.toString(bytes)); + } + } + + static void assertBufferTypeCollection(NetworkBuffer.@NotNull Type type, @NotNull Collection value) { + assertBufferTypeCollection(type, value, null); + } + + interface Action { + void write(@NotNull NetworkBuffer buffer, NetworkBuffer.@NotNull Type type, @UnknownNullability T value); + + T read(@NotNull NetworkBuffer buffer, NetworkBuffer.@NotNull Type type); + } +} diff --git a/src/test/java/net/minestom/server/network/SocketWriteTest.java b/src/test/java/net/minestom/server/network/SocketWriteTest.java new file mode 100644 index 000000000..481305f40 --- /dev/null +++ b/src/test/java/net/minestom/server/network/SocketWriteTest.java @@ -0,0 +1,107 @@ +package net.minestom.server.network; + +import net.minestom.server.network.packet.server.ServerPacket; +import net.minestom.server.utils.ObjectPool; +import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.Utils; +import net.minestom.server.utils.binary.BinaryWriter; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class SocketWriteTest { + + record IntPacket(int value) implements ServerPacket { + @Override + public void write(@NotNull BinaryWriter writer) { + writer.writeInt(value); + } + + @Override + public int getId() { + return 1; + } + } + + record CompressiblePacket(String value) implements ServerPacket { + @Override + public void write(@NotNull BinaryWriter writer) { + writer.writeSizedString(value); + } + + @Override + public int getId() { + return 1; + } + } + + @Test + public void writeSingleUncompressed() { + var packet = new IntPacket(5); + + var buffer = ObjectPool.PACKET_POOL.get(); + PacketUtils.writeFramedPacket(buffer, packet, false); + + // 3 bytes length [var-int] + 1 byte packet id [var-int] + 4 bytes int + // The 3 bytes var-int length is hardcoded for performance purpose, could change in the future + assertEquals(3 + 1 + 4, buffer.position(), "Invalid buffer position"); + } + + @Test + public void writeMultiUncompressed() { + var packet = new IntPacket(5); + + var buffer = ObjectPool.PACKET_POOL.get(); + PacketUtils.writeFramedPacket(buffer, packet, false); + PacketUtils.writeFramedPacket(buffer, packet, false); + + // 3 bytes length [var-int] + 1 byte packet id [var-int] + 4 bytes int + // The 3 bytes var-int length is hardcoded for performance purpose, could change in the future + assertEquals((3 + 1 + 4) * 2, buffer.position(), "Invalid buffer position"); + } + + @Test + public void writeSingleCompressed() { + var string = "Hello world!".repeat(200); + var stringLength = string.getBytes(StandardCharsets.UTF_8).length; + var lengthLength = Utils.getVarIntSize(stringLength); + + var packet = new CompressiblePacket(string); + + var buffer = ObjectPool.PACKET_POOL.get(); + PacketUtils.writeFramedPacket(buffer, packet, true); + + // 3 bytes packet length [var-int] + 3 bytes data length [var-int] + 1 byte packet id [var-int] + payload + // The 3 bytes var-int length is hardcoded for performance purpose, could change in the future + assertNotEquals(3 + 3 + 1 + lengthLength + stringLength, buffer.position(), "Buffer position does not account for compression"); + } + + @Test + public void writeSingleCompressedSmall() { + var packet = new IntPacket(5); + + var buffer = ObjectPool.PACKET_POOL.get(); + PacketUtils.writeFramedPacket(buffer, packet, true); + + // 3 bytes packet length [var-int] + 3 bytes data length [var-int] + 1 byte packet id [var-int] + 4 bytes int + // The 3 bytes var-int length is hardcoded for performance purpose, could change in the future + assertEquals(3 + 3 + 1 + 4, buffer.position(), "Invalid buffer position"); + } + + @Test + public void writeMultiCompressedSmall() { + var packet = new IntPacket(5); + + var buffer = ObjectPool.PACKET_POOL.get(); + PacketUtils.writeFramedPacket(buffer, packet, true); + PacketUtils.writeFramedPacket(buffer, packet, true); + + // 3 bytes packet length [var-int] + 3 bytes data length [var-int] + 1 byte packet id [var-int] + 4 bytes int + // The 3 bytes var-int length is hardcoded for performance purpose, could change in the future + assertEquals((3 + 3 + 1 + 4) * 2, buffer.position(), "Invalid buffer position"); + } +}