package net.minestom.server.utils; import it.unimi.dsi.fastutil.ints.IntIntPair; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.minestom.server.MinecraftServer; import net.minestom.server.Viewable; import net.minestom.server.adventure.MinestomAdventure; import net.minestom.server.adventure.audience.PacketGroupingAudience; import net.minestom.server.entity.Player; import net.minestom.server.listener.manager.PacketListenerManager; import net.minestom.server.network.packet.FramedPacket; import net.minestom.server.network.packet.server.ComponentHoldingServerPacket; 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.network.socket.Server; import net.minestom.server.utils.binary.BinaryBuffer; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.PooledBuffers; import net.minestom.server.utils.callback.validator.PlayerValidator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.zip.Deflater; /** * 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 PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager(); private static final ThreadLocal LOCAL_DEFLATER = ThreadLocal.withInitial(Deflater::new); /// Local buffers private static final LocalCache PACKET_BUFFER = LocalCache.get("packet-buffer", Server.MAX_PACKET_SIZE); private static final LocalCache LOCAL_BUFFER = LocalCache.get("local-buffer", Server.MAX_PACKET_SIZE); // Viewable packets private static final Object VIEWABLE_PACKET_LOCK = new Object(); private static final Map VIEWABLE_STORAGE_MAP = new WeakHashMap<>(); private PacketUtils() { } @ApiStatus.Internal @ApiStatus.Experimental public static ByteBuffer localBuffer() { return LOCAL_BUFFER.get(); } /** * 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) audience).getPlayerConnection().sendPacket(packet); } else if (audience instanceof PacketGroupingAudience) { PacketUtils.sendGroupedPacket(((PacketGroupingAudience) audience).getPlayers(), packet); } else if (audience instanceof ForwardingAudience.Single) { PacketUtils.sendPacket(((ForwardingAudience.Single) audience).audience(), packet); } else if (audience instanceof ForwardingAudience) { for (Audience member : ((ForwardingAudience) audience).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 playerValidator optional callback to check if a specify player of {@code players} should receive the packet */ public static void sendGroupedPacket(@NotNull Collection players, @NotNull ServerPacket packet, @NotNull PlayerValidator playerValidator) { if (players.isEmpty()) return; // work out if the packet needs to be sent individually due to server-side translating boolean needsTranslating = false; if (MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION && packet instanceof ComponentHoldingServerPacket) { needsTranslating = ComponentUtils.areAnyTranslatable(((ComponentHoldingServerPacket) packet).components()); } if (MinecraftServer.hasGroupedPacket() && !needsTranslating) { // Send grouped packet... if (!PACKET_LISTENER_MANAGER.processServerPacket(packet, players)) return; final ByteBuffer finalBuffer = createFramedPacket(packet).flip(); final FramedPacket framedPacket = new FramedPacket(packet.getId(), finalBuffer, packet); // Send packet to all players for (Player player : players) { if (!player.isOnline() || !playerValidator.isValid(player)) continue; final PlayerConnection connection = player.getPlayerConnection(); if (connection instanceof PlayerSocketConnection) { ((PlayerSocketConnection) connection).write(framedPacket); } else { connection.sendPacket(packet); } } } else { // Write the same packet for each individual players for (Player player : players) { if (!player.isOnline() || !playerValidator.isValid(player)) continue; player.getPlayerConnection().sendPacket(packet, false); } } } /** * Same as {@link #sendGroupedPacket(Collection, ServerPacket, PlayerValidator)} * but with the player validator sets to null. * * @see #sendGroupedPacket(Collection, ServerPacket, PlayerValidator) */ public static void sendGroupedPacket(@NotNull Collection players, @NotNull ServerPacket packet) { sendGroupedPacket(players, packet, player -> true); } public static void broadcastPacket(@NotNull ServerPacket packet) { sendGroupedPacket(MinecraftServer.getConnectionManager().getOnlinePlayers(), packet); } @ApiStatus.Experimental public static void prepareViewablePacket(@NotNull Viewable viewable, @NotNull ServerPacket serverPacket, @Nullable Player player) { if (player != null && !player.isAutoViewable()) { // Operation cannot be optimized player.sendPacketToViewers(serverPacket); return; } ViewableStorage viewableStorage; synchronized (VIEWABLE_PACKET_LOCK) { viewableStorage = VIEWABLE_STORAGE_MAP.computeIfAbsent(viewable, ViewableStorage::new); } viewableStorage.append(serverPacket, player != null ? player.getPlayerConnection() : null); } @ApiStatus.Experimental public static void prepareViewablePacket(@NotNull Viewable viewable, @NotNull ServerPacket serverPacket) { prepareViewablePacket(viewable, serverPacket, null); } @ApiStatus.Internal public static void flush() { synchronized (VIEWABLE_PACKET_LOCK) { for (ViewableStorage viewableStorage : VIEWABLE_STORAGE_MAP.values()) { viewableStorage.process(); } } } public static void writeFramedPacket(@NotNull ByteBuffer buffer, @NotNull ServerPacket packet, boolean compression) { if (!compression) { // Uncompressed format https://wiki.vg/Protocol#Without_compression final int lengthIndex = Utils.writeEmptyVarIntHeader(buffer); Utils.writeVarInt(buffer, packet.getId()); packet.write(new BinaryWriter(buffer)); final int finalSize = buffer.position() - (lengthIndex + 3); Utils.writeVarIntHeader(buffer, lengthIndex, finalSize); return; } // Compressed format https://wiki.vg/Protocol#With_compression final int compressedIndex = Utils.writeEmptyVarIntHeader(buffer); final int uncompressedIndex = Utils.writeEmptyVarIntHeader(buffer); final int contentStart = buffer.position(); Utils.writeVarInt(buffer, packet.getId()); packet.write(BinaryWriter.view(buffer)); // ensure that the buffer is not resized/changed final int packetSize = buffer.position() - contentStart; final boolean compressed = packetSize >= MinecraftServer.getCompressionThreshold(); if (compressed) { // Packet large enough, compress buffer.position(contentStart); final ByteBuffer uncompressedContent = buffer.slice().limit(packetSize); final ByteBuffer uncompressedCopy = localBuffer().put(uncompressedContent).flip(); Deflater deflater = LOCAL_DEFLATER.get(); deflater.setInput(uncompressedCopy); deflater.finish(); deflater.deflate(buffer); deflater.reset(); } // Packet header (Packet + Data Length) Utils.writeVarIntHeader(buffer, compressedIndex, buffer.position() - uncompressedIndex); Utils.writeVarIntHeader(buffer, uncompressedIndex, compressed ? packetSize : 0); } public static ByteBuffer createFramedPacket(@NotNull ServerPacket packet, boolean compression) { ByteBuffer buffer = PACKET_BUFFER.get(); writeFramedPacket(buffer, packet, compression); return buffer; } public static ByteBuffer createFramedPacket(@NotNull ServerPacket packet) { return createFramedPacket(packet, MinecraftServer.getCompressionThreshold() > 0); } @ApiStatus.Internal public static FramedPacket allocateTrimmedPacket(@NotNull ServerPacket packet) { final ByteBuffer temp = PacketUtils.createFramedPacket(packet).flip(); final ByteBuffer buffer = ByteBuffer.allocateDirect(temp.remaining()) .put(temp).flip().asReadOnlyBuffer(); return new FramedPacket(packet.getId(), buffer, packet); } @ApiStatus.Internal public static final class LocalCache { private static final Map CACHES = new ConcurrentHashMap<>(); private final String name; private final ThreadLocal cache; private LocalCache(String name, int size) { this.name = name; this.cache = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(size)); } public static LocalCache get(String name, int size) { return CACHES.computeIfAbsent(name, s -> new LocalCache(s, size)); } public String name() { return name; } public ByteBuffer get() { return cache.get().clear(); } } private static final class ViewableStorage { private final WeakReference viewable; private final Map> entityIdMap = new HashMap<>(); private final BinaryBuffer buffer = PooledBuffers.get(); private ViewableStorage(Viewable viewable) { this.viewable = new WeakReference<>(viewable); PooledBuffers.registerBuffer(this, buffer); } private synchronized void append(ServerPacket serverPacket, PlayerConnection connection) { final ByteBuffer framedPacket = createFramedPacket(serverPacket).flip(); final int packetSize = framedPacket.limit(); if (packetSize >= buffer.capacity()) { process(new SingleEntry(framedPacket, connection)); return; } if (!buffer.canWrite(packetSize)) process(); final int start = buffer.writerOffset(); buffer.write(framedPacket); final int end = buffer.writerOffset(); if (connection != null) { List list = entityIdMap.computeIfAbsent(connection, con -> new ArrayList<>()); list.add(IntIntPair.of(start, end)); } } private synchronized void process(@Nullable SingleEntry singleEntry) { if (buffer.writerOffset() == 0) return; // TODO: there is nothing in the buffer, remove from VIEWABLE_STORAGE_MAP final Viewable viewable = this.viewable.get(); if (viewable == null) return; for (Player player : viewable.getViewers()) { PlayerConnection connection = player.getPlayerConnection(); Consumer writer = connection instanceof PlayerSocketConnection ? ((PlayerSocketConnection) connection)::write : byteBuffer -> { // TODO for non-socket connection }; int lastWrite = 0; final List pairs = entityIdMap.get(connection); if (pairs != null) { for (IntIntPair pair : pairs) { final int start = pair.leftInt(); if (start != lastWrite) { ByteBuffer slice = buffer.asByteBuffer(lastWrite, start - lastWrite); writer.accept(slice); } lastWrite = pair.rightInt(); } } // Write remaining final int remaining = buffer.writerOffset() - lastWrite; if (remaining > 0) { ByteBuffer remainSlice = buffer.asByteBuffer(lastWrite, remaining); writer.accept(remainSlice); } // Handle single entry if (singleEntry != null && !Objects.equals(singleEntry.exception, connection)) { writer.accept(singleEntry.buffer.position(0)); } } // Clear state this.entityIdMap.clear(); this.buffer.clear(); } private void process() { process(null); } private static final class SingleEntry { private final ByteBuffer buffer; private final PlayerConnection exception; public SingleEntry(ByteBuffer buffer, PlayerConnection exception) { this.buffer = buffer; this.exception = exception; } } } }