All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.minestom.server.utils.PacketUtils Maven / Gradle / Ivy

There is a newer version: 7320437640
Show newest version
package net.minestom.server.utils;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.minestom.server.MinecraftServer;
import net.minestom.server.ServerFlag;
import net.minestom.server.Viewable;
import net.minestom.server.adventure.ComponentHolder;
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.ConnectionState;
import net.minestom.server.network.NetworkBuffer;
import net.minestom.server.network.packet.server.CachedPacket;
import net.minestom.server.network.packet.server.FramedPacket;
import net.minestom.server.network.packet.server.SendablePacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.utils.binary.BinaryBuffer;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

/**
 * Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuffer}
 * for network processing.
 * 

* Note that all methods are mostly internal and can change at any moment. * This is due to their very unsafe nature (use of local buffers as cache) and their potential performance impact. * Be sure to check the implementation code. */ public final class PacketUtils { private static final ThreadLocal LOCAL_DEFLATER = ThreadLocal.withInitial(Deflater::new); // Viewable packets private static final Cache VIEWABLE_STORAGE_MAP = Caffeine.newBuilder().weakKeys().build(); private PacketUtils() { } /** * Sends a packet to an audience. This method performs the following steps in the * following order: *

    *
  1. If {@code audience} is a {@link Player}, send the packet to them.
  2. *
  3. Otherwise, if {@code audience} is a {@link PacketGroupingAudience}, call * {@link #sendGroupedPacket(Collection, ServerPacket)} on the players that the * grouping audience contains.
  4. *
  5. Otherwise, if {@code audience} is a {@link ForwardingAudience.Single}, * call this method on the single audience inside the forwarding audience.
  6. *
  7. Otherwise, if {@code audience} is a {@link ForwardingAudience}, call this * method for each audience member of the forwarding audience.
  8. *
  9. Otherwise, do nothing.
  10. *
* * @param audience the audience * @param packet the packet */ @SuppressWarnings("OverrideOnly") // we need to access the audiences inside ForwardingAudience public static void sendPacket(@NotNull Audience audience, @NotNull ServerPacket packet) { if (audience instanceof Player player) { player.sendPacket(packet); } else if (audience instanceof PacketGroupingAudience groupingAudience) { PacketUtils.sendGroupedPacket(groupingAudience.getPlayers(), packet); } else if (audience instanceof ForwardingAudience.Single singleAudience) { PacketUtils.sendPacket(singleAudience.audience(), packet); } else if (audience instanceof ForwardingAudience forwardingAudience) { for (Audience member : forwardingAudience.audiences()) { PacketUtils.sendPacket(member, packet); } } } /** * Sends a {@link ServerPacket} to multiple players. *

* Can drastically improve performance since the packet will not have to be processed as much. * * @param players the players to send the packet to * @param packet the packet to send to the players * @param predicate predicate to ignore specific players */ public static void sendGroupedPacket(@NotNull Collection players, @NotNull ServerPacket packet, @NotNull Predicate predicate) { final var sendablePacket = shouldUseCachePacket(packet) ? new CachedPacket(packet) : packet; players.forEach(player -> { if (predicate.test(player)) player.sendPacket(sendablePacket); }); } /** * Checks if the {@link ServerPacket} is suitable to be wrapped into a {@link CachedPacket}. * Note: {@link ServerPacket.ComponentHolding}s are not translated inside a {@link CachedPacket}. * * @see CachedPacket#body(ConnectionState) * @see PlayerSocketConnection#writePacketSync(SendablePacket, boolean) */ static boolean shouldUseCachePacket(final @NotNull ServerPacket packet) { if (!MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION) return ServerFlag.GROUPED_PACKET; if (!(packet instanceof ServerPacket.ComponentHolding holder)) return ServerFlag.GROUPED_PACKET; return !containsTranslatableComponents(holder); } private static boolean containsTranslatableComponents(final @NotNull ComponentHolder holder) { for (final Component component : holder.components()) { if (isTranslatable(component)) return true; } return false; } private static boolean isTranslatable(final @NotNull Component component) { if (component instanceof TranslatableComponent) return true; final var children = component.children(); if (children.isEmpty()) return false; for (final Component child : children) { if (isTranslatable(child)) return true; } return false; } /** * Same as {@link #sendGroupedPacket(Collection, ServerPacket, Predicate)} * but with the player validator sets to null. * * @see #sendGroupedPacket(Collection, ServerPacket, Predicate) */ public static void sendGroupedPacket(@NotNull Collection players, @NotNull ServerPacket packet) { sendGroupedPacket(players, packet, player -> true); } public static void broadcastPlayPacket(@NotNull ServerPacket packet) { sendGroupedPacket(MinecraftServer.getConnectionManager().getOnlinePlayers(), packet); } @ApiStatus.Experimental public static void prepareViewablePacket(@NotNull Viewable viewable, @NotNull ServerPacket serverPacket, @Nullable Entity entity) { if (entity != null && !entity.hasPredictableViewers()) { // Operation cannot be optimized entity.sendPacketToViewers(serverPacket); return; } if (!ServerFlag.VIEWABLE_PACKET) { sendGroupedPacket(viewable.getViewers(), serverPacket, value -> !Objects.equals(value, entity)); return; } final Player exception = entity instanceof Player ? (Player) entity : null; ViewableStorage storage = VIEWABLE_STORAGE_MAP.get(viewable, (unused) -> new ViewableStorage()); storage.append(viewable, serverPacket, exception); } @ApiStatus.Experimental public static void prepareViewablePacket(@NotNull Viewable viewable, @NotNull ServerPacket serverPacket) { prepareViewablePacket(viewable, serverPacket, null); } @ApiStatus.Internal public static void flush() { if (ServerFlag.VIEWABLE_PACKET) { VIEWABLE_STORAGE_MAP.asMap().entrySet().parallelStream().forEach(entry -> entry.getValue().process(entry.getKey())); } } @ApiStatus.Internal public static @Nullable BinaryBuffer readPackets(@NotNull BinaryBuffer readBuffer, boolean compressed, BiConsumer payloadConsumer) throws DataFormatException { BinaryBuffer remaining = null; ByteBuffer pool = ObjectPool.PACKET_POOL.get(); while (readBuffer.readableBytes() > 0) { final var beginMark = readBuffer.mark(); try { // Ensure that the buffer contains the full packet (or wait for next socket read) final int packetLength = readBuffer.readVarInt(); final int readerStart = readBuffer.readerOffset(); if (!readBuffer.canRead(packetLength)) { // Integrity fail throw new BufferUnderflowException(); } // Read packet https://wiki.vg/Protocol#Packet_format BinaryBuffer content = readBuffer; int decompressedSize = packetLength; if (compressed) { final int dataLength = readBuffer.readVarInt(); final int payloadLength = packetLength - (readBuffer.readerOffset() - readerStart); if (payloadLength < 0) { throw new DataFormatException("Negative payload length " + payloadLength); } if (dataLength == 0) { // Data is too small to be compressed, payload is following decompressedSize = payloadLength; } else { // Decompress to content buffer content = BinaryBuffer.wrap(pool); decompressedSize = dataLength; Inflater inflater = new Inflater(); // TODO: Pool? inflater.setInput(readBuffer.asByteBuffer(readBuffer.readerOffset(), payloadLength)); inflater.inflate(content.asByteBuffer(0, dataLength)); inflater.reset(); } } // Slice packet ByteBuffer payload = content.asByteBuffer(content.readerOffset(), decompressedSize); final int packetId = Utils.readVarInt(payload); try { payloadConsumer.accept(packetId, payload); } catch (Exception e) { throw new RuntimeException(e); } // Position buffer to read the next packet readBuffer.readerOffset(readerStart + packetLength); } catch (BufferUnderflowException e) { readBuffer.reset(beginMark); remaining = BinaryBuffer.copy(readBuffer); break; } } ObjectPool.PACKET_POOL.add(pool); return remaining; } public static void writeFramedPacket(@NotNull ConnectionState state, @NotNull ByteBuffer buffer, @NotNull ServerPacket packet, boolean compression) { writeFramedPacket(buffer, packet.getId(state), packet, compression ? MinecraftServer.getCompressionThreshold() : 0); } public static void writeFramedPacket(@NotNull ByteBuffer buffer, int id, @NotNull NetworkBuffer.Writer writer, int compressionThreshold) { NetworkBuffer networkBuffer = new NetworkBuffer(buffer, false); if (compressionThreshold <= 0) { // Uncompressed format https://wiki.vg/Protocol#Without_compression final int lengthIndex = networkBuffer.skipWrite(3); networkBuffer.write(NetworkBuffer.VAR_INT, id); networkBuffer.write(writer); 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 = networkBuffer.skipWrite(3); final int uncompressedIndex = networkBuffer.skipWrite(3); final int contentStart = networkBuffer.writeIndex(); networkBuffer.write(NetworkBuffer.VAR_INT, id); networkBuffer.write(writer); final int packetSize = networkBuffer.writeIndex() - contentStart; final boolean compressed = packetSize >= compressionThreshold; if (compressed) { // Packet large enough, compress it try (var hold = ObjectPool.PACKET_POOL.hold()) { final ByteBuffer input = hold.get().put(0, buffer, contentStart, packetSize); Deflater deflater = LOCAL_DEFLATER.get(); deflater.setInput(input.limit(packetSize)); deflater.finish(); deflater.deflate(buffer.position(contentStart)); deflater.reset(); networkBuffer.skipWrite(buffer.position() - contentStart); } } // Packet header (Packet + Data Length) Utils.writeVarIntHeader(buffer, compressedIndex, networkBuffer.writeIndex() - uncompressedIndex); Utils.writeVarIntHeader(buffer, uncompressedIndex, compressed ? packetSize : 0); buffer.position(networkBuffer.writeIndex()); } @ApiStatus.Internal public static ByteBuffer createFramedPacket(@NotNull ConnectionState state, @NotNull ByteBuffer buffer, @NotNull ServerPacket packet, boolean compression) { writeFramedPacket(state, buffer, packet, compression); return buffer.flip(); } @ApiStatus.Internal public static ByteBuffer createFramedPacket(@NotNull ConnectionState state, @NotNull ByteBuffer buffer, @NotNull ServerPacket packet) { return createFramedPacket(state, buffer, packet, MinecraftServer.getCompressionThreshold() > 0); } @ApiStatus.Internal public static FramedPacket allocateTrimmedPacket(@NotNull ConnectionState state, @NotNull ServerPacket packet) { try (var hold = ObjectPool.PACKET_POOL.hold()) { final ByteBuffer temp = PacketUtils.createFramedPacket(state, hold.get(), packet); final int size = temp.remaining(); final ByteBuffer buffer = ByteBuffer.allocateDirect(size).put(0, temp, 0, size); return new FramedPacket(packet, buffer); } } private static final class ViewableStorage { // Player id -> list of offsets to ignore (32:32 bits) private final Int2ObjectMap entityIdMap = new Int2ObjectOpenHashMap<>(); private final BinaryBuffer buffer = ObjectPool.BUFFER_POOL.getAndRegister(this); private synchronized void append(Viewable viewable, ServerPacket serverPacket, @Nullable Player exception) { try (var hold = ObjectPool.PACKET_POOL.hold()) { // Viewable storage is only used for play packets, so fine to assume this. final ByteBuffer framedPacket = createFramedPacket(ConnectionState.PLAY, hold.get(), serverPacket); final int packetSize = framedPacket.limit(); if (packetSize >= buffer.capacity()) { process(viewable); for (Player viewer : viewable.getViewers()) { if (!Objects.equals(exception, viewer)) { writeTo(viewer.getPlayerConnection(), framedPacket, 0, packetSize); } } return; } if (!buffer.canWrite(packetSize)) process(viewable); final int start = buffer.writerOffset(); this.buffer.write(framedPacket); final int end = buffer.writerOffset(); if (exception != null) { final long offsets = (long) start << 32 | end & 0xFFFFFFFFL; LongList list = entityIdMap.computeIfAbsent(exception.getEntityId(), id -> new LongArrayList()); list.add(offsets); } } } private synchronized void process(Viewable viewable) { if (buffer.writerOffset() == 0) return; ByteBuffer copy = ByteBuffer.allocateDirect(buffer.writerOffset()); copy.put(buffer.asByteBuffer(0, copy.capacity())); viewable.getViewers().forEach(player -> processPlayer(player, copy)); this.buffer.clear(); this.entityIdMap.clear(); } private void processPlayer(Player player, ByteBuffer buffer) { final int size = buffer.limit(); final PlayerConnection connection = player.getPlayerConnection(); final LongArrayList pairs = entityIdMap.get(player.getEntityId()); if (pairs != null) { // Ensure that we skip the specified parts of the buffer int lastWrite = 0; final long[] elements = pairs.elements(); for (int i = 0; i < pairs.size(); ++i) { final long offsets = elements[i]; final int start = (int) (offsets >> 32); if (start != lastWrite) writeTo(connection, buffer, lastWrite, start - lastWrite); lastWrite = (int) offsets; // End = last 32 bits } if (size != lastWrite) writeTo(connection, buffer, lastWrite, size - lastWrite); } else { // Write all writeTo(connection, buffer, 0, size); } } private static void writeTo(PlayerConnection connection, ByteBuffer buffer, int offset, int length) { if (connection instanceof PlayerSocketConnection socketConnection) { socketConnection.write(buffer, offset, length); return; } // TODO for non-socket connection } } @ApiStatus.Internal public static int invalidPacketState(@NotNull Class packetClass, @NotNull ConnectionState state, @NotNull ConnectionState... expected) { assert expected.length > 0 : "Expected states cannot be empty: " + packetClass; StringBuilder expectedStr = new StringBuilder(); for (ConnectionState connectionState : expected) { expectedStr.append(connectionState).append(", "); } expectedStr.delete(expectedStr.length() - 2, expectedStr.length()); throw new IllegalStateException(String.format("Packet %s is not valid in state %s (only %s)", packetClass.getSimpleName(), state, expectedStr)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy