AnvilRenameHandler.java

package it.fulminazzo.yagl.handler;

import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.yagl.GUIAdapter;
import it.fulminazzo.yagl.scheduler.Scheduler;
import it.fulminazzo.yagl.scheduler.Task;
import it.fulminazzo.yagl.util.NMSUtils;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A special {@link ChannelDuplexHandler} capable of intercepting and
 * reading anvil rename packets from the player.
 */
public final class AnvilRenameHandler extends ChannelDuplexHandler {
    private static final int DEBOUNCE_DELAY = 2;

    private final @NotNull Logger logger;

    private final @NotNull UUID playerId;

    private final @NotNull BiConsumer<Player, String> handler;

    private @Nullable Task handleTask;

    /**
     * Instantiates a new Anvil rename handler.
     *
     * @param playerId the player id
     * @param handler  the action to execute upon successful reading
     */
    public AnvilRenameHandler(final @NotNull UUID playerId,
                              final @NotNull BiConsumer<Player, String> handler) {
        this.logger = GUIAdapter.getProvidingPlugin().getLogger();
        this.playerId = playerId;
        this.handler = handler;
    }

    @Override
    public void channelRead(final @NotNull ChannelHandlerContext context,
                            final @NotNull Object packet) throws Exception {
        Player player = getPlayer();
        try {
            String packetName = packet.getClass().getSimpleName();
            Refl<?> packetRefl = new Refl<>(packet);
            final String name;

            switch (packetName) {
                // 1.8.8, 1.9.4, 1.10.2, 1.11.2, 1.12.2
                case "PacketPlayInCustomPayload":
                    String type = packetRefl.getFieldObject(String.class);
                    if ("MC|ItemName".equals(type)) {
                        Refl<?> refl = packetRefl.getFieldRefl("b");
                        ByteBuf buf = refl.getFieldObject(ByteBuf.class);
                        ByteBuf copy = buf.copy();

                        // First read the size of the buffer
                        int size = copy.readByte();

                        byte[] data = new byte[size];
                        copy.readBytes(data);
                        name = new String(data);
                    } else return; // Other packet
                    break;
                // 1.13.2, 1.14.4, 1.15.2, 1.16.5, 1.17.1, 1.18.2, 1.19.4
                case "PacketPlayInItemName":
                    name = packetRefl.getFieldObject(String.class);
                    break;
                // 1.20.6, 1.21.4, ...
                case "ServerboundRenameItemPacket":
                    name = packetRefl.getFieldObject(String.class);
                    break;
                // Other packet
                default:
                    return;
            }

            stopHandleTask();

            this.handleTask = Scheduler.getScheduler().runLaterAsync(
                    GUIAdapter.getProvidingPlugin(),
                    () -> this.handler.accept(player, name),
                    DEBOUNCE_DELAY
            );
        } catch (Exception e) {
            // Usually catching Exception is bad, but in this case is necessary to avoid the player getting kicked
            this.logger.log(
                    Level.WARNING,
                    String.format("An error occurred while reading a packet from player '%s': %s",
                            player.getName(), e.getMessage()),
                    e
            );
        } finally {
            super.channelRead(context, packet);
        }
    }

    /**
     * Stops the {@link #handleTask} if present.
     */
    void stopHandleTask() {
        if (this.handleTask != null) {
            this.handleTask.cancel();
            this.handleTask = null;
        }
    }

    /**
     * Inserts the current handler in the player's channel.
     */
    public void inject() {
        Channel channel = NMSUtils.getPlayerChannel(getPlayer());
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addBefore("packet_handler", getName(), this);
    }

    /**
     * Removes the current handler from the player's channel.
     */
    public void remove() {
        Channel channel = NMSUtils.getPlayerChannel(getPlayer());
        channel.eventLoop().submit(() -> channel.pipeline().remove(getName()));
        stopHandleTask();
    }

    /**
     * Gets name of the current handler.
     *
     * @return the name
     */
    public @NotNull String getName() {
        return String.format("%s-%s",
                getClass().getSimpleName(),
                this.playerId.toString().replace("-", "_")
        );
    }

    /**
     * Checks if the current handler belongs to the specified player.
     *
     * @param player the player
     * @return true if the {@link #playerId} matches
     */
    public boolean belongsTo(final @NotNull Player player) {
        return player.getUniqueId().equals(this.playerId);
    }

    /**
     * Gets the player.
     *
     * @return the player
     */
    @NotNull Player getPlayer() {
        Player player = Bukkit.getPlayer(this.playerId);
        if (player == null)
            throw new IllegalStateException(String.format("Player '%s' is not online", this.playerId));
        return player;
    }

    @Override
    public String toString() {
        return String.format("%s{playerId=%s}", getClass().getSimpleName(), this.playerId);
    }

}