Resource SignMenu [1.15.2] - Get Player Sign Input!

Discussion in 'Spigot Plugin Development' started by FrostedSnowman, Jun 19, 2017.

  1. FrostedSnowman

    Resource Staff

    [​IMG]

    TESTED VERSIONS: 1.15.2 (RECOMMENDED BUILD)

    WHAT IS IT?
    SignMenu allows you to easily create client sided sign editor menus. As of current versions, use of ProtocolLib (4.4.0-b421) is required.



    HOW DO I USE IT?
    To start, you'll want to keep a single instance of the class somewhere in your project where you can easily access it.

    In the example below, I will be defining the instance in my main class.

    Code (Java):
    public class MyPlugin extends JavaPlugin {

          private SignMenuFactory signMenuFactory;

          @Override
          public void onEnable() {
               this.signMenuFactory = new SignMenuFactory(this);
           }

          public SignMenuFactory getSignMenuFactory() {
               return this.signMenuFactory;
          }
    }
    Now to show a menu to a player:
    Code (Java):
    //yay for lambdas!
    public void foo(Player target) {
         this.signMenuFactory
                 .newMenu(Lists.newArrayList("&a&lExample", "&4&lSign", "&4", "&3"))
                 .reopenIfFail()
                 .response((player, lines) -> {
                     if (player.getName().equals("Notch") && lines[0].equals("Hello")) {
                         return true;
                     }
                     return false; // failure. becaues reopenIfFail was called, menu will reopen when closed.
                 })
                 .open(target);
    }

    And would appear in game as (with each of the lines outputted to the player):[​IMG]



    WHY KEEP A REFERENCE?

    Well, why don't we just create throw away instances to simple open a sign and receive input from it?

    Good question! This is because we do not want our packet listeners to initially stack on top of one another, this has the same result as registering Bukkit API listeners more than once.

    Initially, I thought of doing this, however, it would be much easier to have a reference to the object itself and have it self managing instead of having to inject your own main class instance whenever needed in certain parts of the source code, as well as not wanting a messy check determining if the packet adapter has been registered already.



    SOURCE CODE v4.0

    Code (Java):
    import com.comphenix.protocol.PacketType;
    import com.comphenix.protocol.ProtocolLibrary;
    import com.comphenix.protocol.events.PacketAdapter;
    import com.comphenix.protocol.events.PacketContainer;
    import com.comphenix.protocol.events.PacketEvent;
    import com.comphenix.protocol.wrappers.BlockPosition;
    import com.comphenix.protocol.wrappers.nbt.NbtCompound;
    import org.bukkit.Bukkit;
    import org.bukkit.ChatColor;
    import org.bukkit.Location;
    import org.bukkit.Material;
    import org.bukkit.entity.Player;
    import org.bukkit.plugin.Plugin;

    import java.lang.reflect.InvocationTargetException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.function.BiPredicate;
    import java.util.stream.IntStream;

    public final class SignMenuFactory {

        private static final int ACTION_INDEX = 9;
        private static final int SIGN_LINES = 4;

        private static final String NBT_FORMAT = "{\"text\":\"%s\"}";
        private static final String NBT_BLOCK_ID = "minecraft:sign";

        private final Plugin plugin;

        private final Map<Player, Menu> inputReceivers;

        public SignMenuFactory(Plugin plugin) {
            this.plugin = plugin;
            this.inputReceivers = new HashMap<>();
            this.listen();
        }

        public Menu newMenu(List<String> text) {
            Objects.requireNonNull(text, "text");
            return new Menu(text);
        }

        private void listen() {
            ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(this.plugin, PacketType.Play.Client.UPDATE_SIGN) {
                @Override
                public void onPacketReceiving(PacketEvent event) {
                    Player player = event.getPlayer();

                    Menu menu = inputReceivers.remove(player);

                    if (menu == null) {
                        return;
                    }
                    event.setCancelled(true);

                    boolean success = menu.response.test(player, event.getPacket().getStringArrays().read(0));

                    if (!success && menu.opensOnFail()) {
                        Bukkit.getScheduler().runTaskLater(plugin, () -> menu.open(player), 2L);
                    }
                    player.sendBlockChange(menu.position.toLocation(player.getWorld()), Material.AIR.createBlockData());
                }
            });
        }

        public class Menu {

            private final List<String> text;

            private BiPredicate<Player, String[]> response;
            private boolean reopenIfFail;

            private BlockPosition position;

            Menu(List<String> text) {
                this.text = text;
            }

            protected BlockPosition getPosition() {
                return this.position;
            }

            public boolean opensOnFail() {
                return this.reopenIfFail;
            }

            public Menu reopenIfFail() {
                this.reopenIfFail = true;
                return this;
            }

            public Menu response(BiPredicate<Player, String[]> response) {
                this.response = response;
                return this;
            }

            public void open(Player player) {
                Objects.requireNonNull(player, "player");
                Location location = player.getLocation();
                this.position = new BlockPosition(location.getBlockX(), location.getBlockY() - 5, location.getBlockZ());

                player.sendBlockChange(this.position.toLocation(location.getWorld()), Material.OAK_SIGN.createBlockData());

                PacketContainer openSign = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.OPEN_SIGN_EDITOR);
                PacketContainer signData = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.TILE_ENTITY_DATA);

                openSign.getBlockPositionModifier().write(0, this.position);

                NbtCompound signNBT = (NbtCompound) signData.getNbtModifier().read(0);

                IntStream.range(0, SIGN_LINES).forEach(line -> signNBT.put("Text" + (line + 1), text.size() > line ? String.format(NBT_FORMAT, color(text.get(line))) : " "));

                signNBT.put("x", this.position.getX());
                signNBT.put("y", this.position.getY());
                signNBT.put("z", this.position.getZ());
                signNBT.put("id", NBT_BLOCK_ID);

                signData.getBlockPositionModifier().write(0, this.position);
                signData.getIntegers().write(0, ACTION_INDEX);
                signData.getNbtModifier().write(0, signNBT);

                try {
                    ProtocolLibrary.getProtocolManager().sendServerPacket(player, signData);
                    ProtocolLibrary.getProtocolManager().sendServerPacket(player, openSign);
                } catch (InvocationTargetException exception) {
                    exception.printStackTrace();
                }
                inputReceivers.put(player, this);
            }

            private String color(String input) {
                return ChatColor.translateAlternateColorCodes('&', input);
            }
        }
    }
     
    #1 FrostedSnowman, Jun 19, 2017
    Last edited: Apr 5, 2020
    • Useful Useful x 15
    • Like Like x 4
    • Agree Agree x 4
    • Winner Winner x 2
  2. Going to test when I get home. Thanks for this! I have wanted to get away from chat input.
     
  3. It is. This is a developer resource.
     
    • Agree Agree x 2
  4. FrostedSnowman

    Resource Staff

    I am surprised you have not seen other resources in the subforum lol
     
    • Agree Agree x 1
  5. It works! This is awesome. Thanks man.
     
  6. FrostedSnowman

    Resource Staff

    No problem! I was thinking of adding some kind of 'animation' to the sign in the next version
     
  7. It's work for me. Good job! Useful Resource.
     
    • Like Like x 1
  8. FrostedSnowman

    Resource Staff

    Thanks! I'm glad you like it.
     
    • Friendly Friendly x 1
  9. This is awesome, I just need to modify it a bit for 1.8
     
  10. Nice resource :p almost makes me want to get into spigot plugin development again.
    Btw ty for using streams, finally, a developer who utilizes Java 8's amazing features.
     
  11. but dad
    strem gret
    use strem almost all ze time
     
    • Agree Agree x 3
  12. FrostedSnowman

    Resource Staff

    Thanks for the feedback!
     
  13. Can you get player input?
     
  14. FrostedSnowman

    Resource Staff

    yes. if you read the tutorial it clearly shows the text that is received back :) (in the snippet where i send the player the text they inputted)
     
  15. That's so usefull actually. This makes chestshops/signshops so much easier. You can make it so the text will stand there what you should type and when you start to type it dissapears. :D
     
  16. Nice resource man
    Maybe one day i will need it so thanks
     
  17. FrostedSnowman

    Resource Staff

    Thanks for the support!
     
  18. What should I change to make it compatible for 1.8.8?
     
  19. FrostedSnowman

    Resource Staff

    Sadly, I will not be supporting older versions :(
     
    • Like Like x 1
  20. JustBru00

    Benefactor

    This looks extremely useful.