Solved Things to do after user input (Chat/Anvil/Sign etc...)

Discussion in 'Spigot Plugin Development' started by i998979, Jan 7, 2020.

  1. Recently I'm coding a gui and there is a feature that can let user enter keyword and do searching like this

    User click on this sign
    2020-01-07_14.41.26.png

    A fake (packet) sign gui will pop-up and the user will be able to enter keyword they want to search
    2020-01-07_14.41.28.png

    So my problem is, what is the best way for me to make 2020-01-07_14.41.26.png this gui shows up again after user pressing "Done" or ESC to exit the sign gui?

    Currently I'm using an extra GUI constructor, accept the original GUI object and the result of the sign gui
    (If you are interested in code, there it is)
    Code (Text):
        // Extra constructor
        public MenuGUI(MenuGUI gui, String input) {
            this(gui.sorting, input, 1);
           
            Bukkit.broadcastMessage(input);
        }
        // Original constructor
        public MenuGUI(MenuSorting sorting, String searching, int page) {
            super(54, "Rooms");
           
            this.rooms = new ArrayList<>();
            this.sorting = sorting;
            if (sorting == null) this.sorting = MenuSorting.NONE;
            this.searching = searching;
            this.page = page;

            .........
    And calling the gui back like this
    Code (Text):
        public SignGUI(Player player, MythicGUI mythicGui, String... display) {
            this.player = player;
            this.mythicGui = mythicGui;
            this.display = display;
            this.result = "";
        }

        public void registerListener() {
            MythicDungeons.protocolManager.addPacketListener(
                listener = new PacketAdapter(plugin, PacketType.Play.Client.UPDATE_SIGN) {
                    @Override
                    public void onPacketReceiving(PacketEvent event) {
                        ...
                        ...
                        ...
                        Bukkit.getScheduler().runTask(plugin, new Runnable() {
                            @Override
                            public void run() {
                                if (mythicGui instanceof CreateGUI) {
                                    CreateGUI createGui = new CreateGUI((CreateGUI) mythicGui, result);
                                    createGui.open(player);
                                }
                                if (mythicGui instanceof MenuGUI) {
                                    MenuGUI menuGui = new MenuGUI((MenuGUI) mythicGui, result);
                                    menuGui.open(player);
                                }
                            }
                        });
                    }
                });
        }
    But it seems like not very efficient and it doesn't seems really make sense.
    (I know I shouldn't compare custom gui like this, but I'm skipping that part bcz i can't even make the whole thing work... so, yea)

    So what should I do after user completes their input?


    (If you are really interested in the code)
    Code (Text):
    public class SignGUI {

        MythicDungeons plugin = MythicDungeons.inst();

        Player player;
        String[] display;
        Location loc;
        Block block;

        PacketListener listener;

        MythicGUI mythicGui;
        String result;

        public SignGUI(Player player, MythicGUI mythicGui, String... display) {
            this.player = player;
            this.mythicGui = mythicGui;
            this.display = display;
            this.result = "";
        }

        public void registerListener() {
            MythicDungeons.protocolManager.addPacketListener(
                listener = new PacketAdapter(plugin, PacketType.Play.Client.UPDATE_SIGN) {
                    @Override
                    public void onPacketReceiving(PacketEvent event) {
                        PacketContainer packet = event.getPacket();
                        Player player = event.getPlayer();
                        String[] input = packet.getStringArrays().read(0);

                        player.sendBlockChange(loc, block.getBlockData());

                        unregisterListener();

                        StringBuilder builder = new StringBuilder();
                        for (int i = 0; i < input.length; i++) {
                            builder.append(input[i]);
                            if (i < input.length - 1)
                                builder.append(" ");
                        }
                        result = builder.toString();

                        Bukkit.getScheduler().runTask(plugin, new Runnable() {
                            @Override
                            public void run() {
                                if (mythicGui instanceof CreateGUI) {
                                    CreateGUI createGui = new CreateGUI((CreateGUI) mythicGui, result);
                                    createGui.open(player);
                                }
                                if (mythicGui instanceof MenuGUI) {
                                    MenuGUI menuGui = new MenuGUI((MenuGUI) mythicGui, result);
                                    menuGui.open(player);
                                }
                            }
                        });
                    }
                });
        }

        public void unregisterListener() {
            MythicDungeons.protocolManager.removePacketListener(listener);
        }

        public void open() {
            registerListener();

            loc = player.getLocation();
            loc.setY(0);
            block = loc.getBlock();

            player.sendBlockChange(loc, Material.SIGN.createBlockData());

            PacketContainer openSignEditor = MythicDungeons.protocolManager.createPacket(
                PacketType.Play.Server.OPEN_SIGN_EDITOR);
            PacketContainer tileEntityData = MythicDungeons.protocolManager.createPacket(
                PacketType.Play.Server.TILE_ENTITY_DATA);

            BlockPosition blockPosition = new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
            openSignEditor.getBlockPositionModifier().write(0, blockPosition);

            NbtCompound signNbt = NbtFactory.ofCompound("MythicDungeons");
            signNbt.put("Text1", "{\"text\":\"" + display[0] + "\"}");
            signNbt.put("Text2", "{\"text\":\"" + display[1] + "\"}");
            signNbt.put("Text3", "{\"text\":\"" + display[2] + "\"}");
            signNbt.put("Text4", "{\"text\":\"" + display[3] + "\"}");
            signNbt.put("id", "minecraft:sign");
            signNbt.put("x", loc.getBlockX());
            signNbt.put("y", loc.getBlockY());
            signNbt.put("z", loc.getBlockZ());

            tileEntityData.getBlockPositionModifier().write(0, blockPosition);
            tileEntityData.getIntegers().write(0, 9);
            tileEntityData.getNbtModifier().write(0, signNbt);

            try {
                MythicDungeons.protocolManager.sendServerPacket(player, tileEntityData);
                MythicDungeons.protocolManager.sendServerPacket(player, openSignEditor);

            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
     
  2. I would try to abstract the sign input.
    When i try to get user input via chat/anvil/sign i always specify a BiConsumer<Player, Info> or something simmilar.
    A bit like ChatInput.apply(Player user, String message, BiConsumer<Player, String> input);
    and use it with a lambda.

    Pseudocode:
    Code (Java):
        public void apply(Player user, String message, BiConsumer<Player, String> infoConsumer) {
            // Send msg
            ...
            // recieve input
            String input;
            // consume result
            infoConsumer.accept(user, unput);
        }
    Usage:
    Code (Java):
    apply(player, message, (player, input) ->{
                // open GUI with new param
            });
    In your case you would have List<String> as data...

    Another way would be creating a data class that encapsulates the player with the result.
    Abstract:

    Code (Java):
    @Data
    public interface PlayerInputResult<T> {

        Player getPlayer();
        T getData();

    }
     
    Implementation with String:

    Code (Java):
    @AllArgsConstructor
    public class ChatInputResult implements PlayerInputResult<String> {

        private final Player player;
        private final String input;

        @Override
        public Player getPlayer() {
            return player;
        }

        @Override
        public String getData() {
            return input;
        }
    }
    Then you just have to write a method that consumes the ChatInputResult.



    You can even go a bit further and do something like:

    Code (Java):
    public abstract class PlayerInput<T> {

        public void request(T initialData, Player player, Consumer<PlayerInputResult<T>> resultConsumer) {
            resultConsumer.accept(produce(initialData, player));
        }

        protected abstract PlayerInputResult<T> produce(T initialData, Player player);

    }

    Code (Java):

    public class ChatInput extends PlayerInput<String> {

        @Override
        protected PlayerInputResult<String> produce(String initialData, Player player) {
            // Get chat input
            String input;
            return new ChatInputResult(player, input);
        }

    }
     
    Abstraction is the key to modular code.
    PS this wont work as you have to supply some data but the whole idea is using supplier and consumer to create a chain of responsibility
     
    #2 7smile7, Jan 7, 2020
    Last edited: Jan 7, 2020
  3. Thanks for the detailed reply but.. this is out of my knowledge and i'm reading java tutorials about this XD :D

    Maybe these are dumb questions but it really confuse me.

    - Will something like this freeze the main thread until the user press "Done" or ESC?
    Code (Text):
    // Somewhere in your code
    SignGui gui = new SignGui()
            .line(Line.SECOND, "Default line2") // Set line 2
            .line(Line.FOURTH, "Default line4") // Set line 4
            .color() // Color user input
            .listener((player, lines) -> // Listen to user input, where player is the submitter and lines a String[4] array
                    player.sendMessage("You entered " + lines[0] + " on line 1."));
    (#1 Reference: https://www.spigotmc.org/threads/1-8-signgui-user-input-with-signs.296013/)
    - AFAIK BiConsumer accepts 2 parameters and returns nothing, BiFunction accepts 2 parameters and return 1 result, so what to determine the use of BiConsumer and BiFunction?
    (#2 Reference: https://github.com/WesJD/AnvilGUI/blob/master/api/src/main/java/net/wesjd/anvilgui/AnvilGUI.java)
    - In #1, it uses BiConsumer and #2 it uses BiFunction, what's the main difference between them and causes SignGUI to use BiConsumer and AnvilGUI uses BiFunction? And how about ChatInput?
     
  4. depends on what the .listener() method does.
    But this looks right. You might have to do some mapping <UUID, BiConsumer<Player, String>>
    then listen for the close event, get the player from the map and consume what is on the sign + the player from the event.
     
  5. I posted the errors and bugs for few times and I just deleted myself because I solved it myself......
    Seems I have to use my brain more before asking stupid questions :)

    Anyways, this is working code, is there any improvements of this?
    Code (Java):
    package to.epac.factorycraft.mythicdungeons.gui.sign;

    import java.lang.reflect.InvocationTargetException;
    import java.util.function.BiConsumer;

    import org.bukkit.Bukkit;
    import org.bukkit.ChatColor;
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.block.Block;
    import org.bukkit.entity.Player;

    import com.comphenix.protocol.PacketType;
    import com.comphenix.protocol.events.PacketAdapter;
    import com.comphenix.protocol.events.PacketContainer;
    import com.comphenix.protocol.events.PacketEvent;
    import com.comphenix.protocol.events.PacketListener;
    import com.comphenix.protocol.wrappers.BlockPosition;
    import com.comphenix.protocol.wrappers.nbt.NbtCompound;
    import com.comphenix.protocol.wrappers.nbt.NbtFactory;

    import to.epac.factorycraft.mythicdungeons.MythicDungeons;

    public class SignGUI {

        MythicDungeons plugin = MythicDungeons.inst();
       
        String[] display;
        Location loc;
        Block block;
       
        BiConsumer<Player, String[]> biConsumer;
        PacketListener listener;
       
        public SignGUI line(int line, String text) {
            if (display == null) display = new String[4];
           
            display[line] = ChatColor.translateAlternateColorCodes('&', text);
           
            return this;
        }
       
        public SignGUI biConsumer(BiConsumer<Player, String[]> biConsumer) {
            this.biConsumer = biConsumer;
           
            return this;
        }
       
        public void show(Player player) {
            loc = player.getLocation();
            loc.setY(0);
            block = loc.getBlock();
           
            player.sendBlockChange(loc, Material.SIGN.createBlockData());
           
            PacketContainer openSignEditor = MythicDungeons.protocolManager.createPacket(
                PacketType.Play.Server.OPEN_SIGN_EDITOR);
           
            BlockPosition blockPosition = new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
            openSignEditor.getBlockPositionModifier().write(0, blockPosition);
           
            PacketContainer tileEntityData = MythicDungeons.protocolManager.createPacket(
                PacketType.Play.Server.TILE_ENTITY_DATA);
           
            NbtCompound signNbt = NbtFactory.ofCompound("MythicDungeons");
            signNbt.put("Text1", "{\"text\":\"" + display[0] + "\"}");
            signNbt.put("Text2", "{\"text\":\"" + display[1] + "\"}");
            signNbt.put("Text3", "{\"text\":\"" + display[2] + "\"}");
            signNbt.put("Text4", "{\"text\":\"" + display[3] + "\"}");
            signNbt.put("id", "minecraft:sign");
            signNbt.put("x", loc.getBlockX());
            signNbt.put("y", loc.getBlockY());
            signNbt.put("z", loc.getBlockZ());
           
            tileEntityData.getBlockPositionModifier().write(0, blockPosition);
            tileEntityData.getIntegers().write(0, 9);
            tileEntityData.getNbtModifier().write(0, signNbt);
           
            try {
                MythicDungeons.protocolManager.sendServerPacket(player, tileEntityData);
                MythicDungeons.protocolManager.sendServerPacket(player, openSignEditor);
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
           
            MythicDungeons.protocolManager.addPacketListener(listener = new PacketAdapter(plugin, PacketType.Play.Client.UPDATE_SIGN) {
                @Override
                    public void onPacketReceiving(PacketEvent event) {
                    PacketContainer packet = event.getPacket();
                    Player player = event.getPlayer();
                    String[] input = packet.getStringArrays().read(0);
                   
                    player.sendBlockChange(loc, block.getBlockData());
                   
                    unregisterListener();
                   
                    StringBuilder builder = new StringBuilder();
                    for (int i = 0; i < input.length; i++) {
                        builder.append(input[i]);
                        if (i < input.length - 1)
                            builder.append(" ");
                    }
                   
                    Bukkit.getScheduler().runTask(plugin, (Runnable)() -> {
                        biConsumer.accept(player, input);
                    });
                }
            });
        }
       
        public void unregisterListener() {
            MythicDungeons.protocolManager.removePacketListener(listener);
        }
    }
    Usage
    Code (Java):
                SignGUI signgui = new SignGUI()
                        .line(0, "")
                        .line(1, "^^^^^^^^^^^^^^^^")
                        .line(2, "Please enter")
                        .line(3, "dungeon type")
                        .biConsumer((player0, lines) -> {
                            if (lines[0].isEmpty()) {
                                player0.sendMessage(ChatColor.RED + "Please enter dungeon type.");
                                open(player0);
                            }
                            else if (!plugin.getDungeonManager().hasDungeonGroup(lines[0])) {
                                player0.sendMessage(ChatColor.RED + "Invalid dungeon type.");
                                open(player0);
                            }
                            else {
                                DungeonGroup group = plugin.getDungeonManager().getDungeonGroup(lines[0]);
                                CreateGUI createGui = new CreateGUI(group, player0.getUniqueId());
                                createGui.open(player0);
                            }
                        });
                signgui.show(player);
    I'll leave the working code here in case someone like me did lots of searching and didn't find the solution, this might not be the best, but at least working
     
    • Useful Useful x 1