Solved Get players response

Discussion in 'Spigot Plugin Development' started by CodingCyClone, Aug 16, 2021.

  1. I know how to get a players response when asking them only one question but this time I need to ask the player 2 questions but for some reason the second chatHandler does not fire. I need to return a boolean value if the player types "confirm" any ideas?


    Code (Text):
            ItemMeta meta = currentItem.getItemMeta();
            ItemMeta metaCopy = currentItem.getItemMeta();
            player.sendMessage(ChatColor.LIGHT_PURPLE + "What would you like to name your item?");

            chatHandler.registerHandler(player, (p, msg) -> {
                message = msg;
                for(int i=0; i<message.length(); i++){
                    char c = message.charAt(i);
                    if(c == '&'){
                        message = message.replace("" + c + message.charAt(i + 1), getColor("" + c + message.charAt(i + 1)) + "");
                    }
                }
                p.sendMessage("Is this correct? " + message);
                p.sendMessage("If so type \"confirm\"");
                chatHandler.clearHandlers();

                chatHandler.registerHandler(p, (player1, msg2) -> {
                    if(msg2.toLowerCase().contains("confirm")){
                        meta.setDisplayName(message);
                        currentItem.setItemMeta(meta);
                        player1.updateInventory();
                        chatHandler.getHandlers().remove(p);
                    }
                });
            });
    Code (Text):
    public class ChatHandler implements Listener {
        private static final Map<Player, BiConsumer<Player, String>> handlers = new HashMap<>();

        public void registerHandler(Player player, BiConsumer<Player, String> consumer) {
            handlers.put(player, consumer);
        }

        public Map<Player, BiConsumer<Player, String>> getHandlers() {
            return handlers;
        }

        public void clearHandlers() {
            handlers.clear();
        }
       

        @EventHandler
        public void onChat(AsyncPlayerChatEvent event) {
            Player p = event.getPlayer();
            if(handlers.containsKey(p)) {
                handlers.get(p).accept(p, event.getMessage());
                handlers.remove(p);
                event.setCancelled(true);
            }
        }

        public String getMessage() {
            return handlers.toString();
        }

    }
     
  2. HashSet<Player>, runnable on 30 seconds or what long you want and it's solved
     
  3. What exactly do you mean I have not used a runnable for anything that has to do with chat. In the past I have always used the code shown above but it does not work with a 2nd reply. How can I set it up?
     
  4. The Conversation API was built for exactly this purpose. Google it or ask me and I can give you an example
     
    • Agree Agree x 1
  5. I would love an example!
     
  6. First we create the prompts (questions and responses). You want to get the item name so you can extend StringPrompt
    Code (Java):
    public class ItemNamePrompt extends StringPrompt {

        @NotNull
        @Override
        public String getPromptText(@NotNull ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }

        @Nullable
        @Override
        public Prompt acceptInput(@NotNull ConversationContext context, @Nullable String input) {
            //translate color codes
            //either store them in your own data structure somewhere or use ConversationContext present for every conversation
            context.getAllSessionData().put("itemname", translatedInput);
            return new ConfirmItemNamePrompt();
        }
    }
    Note that you don't have to use the ConversationContext. You can also create a constructor in this class where you pass an own object where you can save the data.
    Next you want to confirm the input. Instead of writing exactly "confirm" you can use any sort of boolean text (e.g. 1/0, true/false, on/off, etc.)
    Code (Java):
        @Nullable
        @Override
        protected Prompt acceptValidatedInput(@NotNull ConversationContext context, boolean input) {
            return input ? END_OF_CONVERSATION : new ItemNamePrompt(); //if the user said no, put them back to the beginning
        }

        @NotNull
        @Override
        public String getPromptText(@NotNull ConversationContext context) {
            return context.getSessionData("itemname").toString();
        }
    }
    Lastly you need a ConversationFactory to build and start the Conversation
    Code (Java):
            ConversationFactory cf = new ConversationFactory(plugin);
            Conversation conversation = cf.thatExcludesNonPlayersWithMessage("Only players can choose item names!")
                    .withEscapeSequence("exit") //if the user types exit he leaves the conversation
                    .withFirstPrompt(new ItemNamePrompt())
                    .buildConversation(player);
            conversation.begin();
    There might be some more logic required to handle players leaving the conversation. You can add a ConversationAbandonedListener to listen for that case.
     
  7. I am a little confused how to make this work. What I am hearing is that I should call the getPromptText method from where I want to start the conversation but then where do I go from there? Should I create a new method inside this chatting class that starts a conversation with that player by using the code you provided at the end? I am not sure what ConversationContext is so I dont really know what I need to pass in as a parameter. Also what does return new ConfirmItemNamePrompt(); do because its showing up as red.
     
  8. No, spigot handles all of that for you.
    By calling begin() the player receives the text you set in getPromptText inside the ItemNamePrompt class.
    If a user types an answer to that question this is called from spigot
    By returning a new Prompt (it's the class for the next code block in my initial post, forget to write the class, only its content) this is called
    If the player answers again, spigot calls this method

    It just holds a bunch of information relevant for this conversation. This includes a map you can use to store conversation relevant information throughout the conversation.
    Here's also a general tutorial on using Conversations.
     
  9. +1 ConversationAPI is what you're looking for if you don't want to reinvent the wheel
     
  10. I am not sure how all that fits into what I am trying to do for more context here is what I have so far...
    Code (Text):
    public ConversationFactory factory = new ConversationFactory(Enchants.getInstance());


        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }

        @Nullable
        @Override
        public Prompt acceptInput(ConversationContext context, @Nullable String input) {
            Conversable conv = context.getForWhom();
            context.getAllSessionData().put("itemName", input);
            conv.sendRawMessage(ChatColor.LIGHT_PURPLE + "Is this correct? " + input);
            conv.sendRawMessage(ChatColor.LIGHT_PURPLE + "If so type \"confirm\"");
            return null;
        }



        @Nullable
        protected Prompt acceptValidatedInput(ConversationContext context, String input) {
            if(input.equalsIgnoreCase("confirm")) {
                return END_OF_CONVERSATION;
            }
            else {
                new ChatHandler();
            }
            return null;
        }

        public void startConv(Player sender) {
            factory.withFirstPrompt(new ChatHandler()).withEscapeSequence("exit").buildConversation((Conversable) sender).begin();
        }
    I have no idea how any of these is getting called since none of it is tied to a constructor and nothing calls eachother. My plan was to call that startConv() method so I can pass in the sender as a parameter to start the conversation but I am not sure how any of it really works. You said that after the initial message it will always just somehow call acceptValidInput but will that continue to happen for all the future messages after the initial message and if so what is the point in having a separate method for the first input? Anyways before I return
    END_OF_CONVERSATION; how can I return that correct message back to the method I was originally working in because I have no idea what "Prompt" is.

    Lastly, is there a better way to translate a message into a color code then the way I am doing it now because it seems a little heavy but I mean it works.

    Code (Text):
    for(int i=0; i<message.length(); i++){
                    char c = message.charAt(i);
                    if(c == '&'){
                        message = message.replace("" + c + message.charAt(i + 1), getColor("" + c + message.charAt(i + 1)) + "");
                    }
                }

    public ChatColor getColor(String messages) {
            String messageFixed = messages.replace(ChatColor.BOLD.toString(), "");

            switch (messageFixed){
                case "&": return ChatColor.BLACK;
                case "&1": return ChatColor.DARK_BLUE;
                case "&2": return ChatColor.DARK_GREEN;
                case "&3": return ChatColor.DARK_AQUA;
                case "&4": return ChatColor.DARK_RED;
                case "&5": return ChatColor.DARK_PURPLE;
                case "&6": return ChatColor.GOLD;
                case "&7": return ChatColor.GRAY;
                case "&8": return ChatColor.DARK_GRAY;
                case "&9": return ChatColor.BLUE;

                case "&f": return ChatColor.WHITE;
                case "&d": return ChatColor.LIGHT_PURPLE;
                case "&b": return ChatColor.AQUA;
                case "&a": return ChatColor.GREEN;
                case "&e": return ChatColor.YELLOW;
                case "&c": return ChatColor.RED;

                case "&l": return ChatColor.BOLD;
                case "&n": return ChatColor.UNDERLINE;
                case "&o": return ChatColor.ITALIC;
                case "&m": return ChatColor.STRIKETHROUGH;
            }
            return null;
        }
     
  11. Not sure if that's the way it is because you copied everything to paste it here, but you can't just declare these methods. You have to extend from a Prompt (or something which already extends from Prompt).
    The Conversation API is fairly high level. It's easy to use and hides a lot of stuff you don't need. Again, you don't manually call these methods, spigot does.
    That will work.
    Spigot will call this method precisely after spigot has displayed the text from the getPromptText() method from the class AND the user has typed an answer and hit send. But this method won't be called again, because another Prompt will be displayed and therefore another acceptInput method from the new-current Prompt will be called. It's a lifecycle of Prompts. The guide I linked above goes in-depth about this in general
    You stored the text the user wrote inside the ConversationContext. Retrieve it from there.
    You return null here. Instead you need to return another Prompt... the Prompt which Spigot will use next.
    I assume the ChatHandler refers to this?

    ChatColor.translateAlternateColorCodes(colorChar, text) LINK
     
  12. How am I suppose to return another prompt if there is a required convention that uses @Override? Do you want me to just make a random method that asks a question because at that point I could just send the player a message like I am already doing and then return null. Also how am I suppose to retrieve the ConversationContext because I just put "itemName" in as the key and the string input as the value so how can I get the correct players response and where do I call it from? In normal java I would have a map (which is what ConversationContext is besides from the unassay complexity) then I would create getters and setters to retrieve the data but I cant do that here because all the code is hidden... This honestly is the definition of over engineering. Anyways here is what I have because I still dont know how to retrieve the data or if this will even work...
    Code (Text):
     ChatHandler chatHandler = new ChatHandler();
     chatHandler.startConv(player);
    Code (Text):
    public ConversationFactory factory = new ConversationFactory(Enchants.getInstance());

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }


        @Nullable
        @Override
        public Prompt acceptInput(ConversationContext context, @Nullable String input) {
            Conversable conv = context.getForWhom();
            context.getAllSessionData().put("itemName", input);
            conv.sendRawMessage(ChatColor.LIGHT_PURPLE + "Is this correct? " + input);
            conv.sendRawMessage(ChatColor.LIGHT_PURPLE + "If so type \"confirm\"");
            return null;
        }


        @Nullable
        protected Prompt acceptValidatedInput(ConversationContext context, String input) {
            if(input.equalsIgnoreCase("confirm")) {
                return END_OF_CONVERSATION;
            }
            else {
                getPromptText(context);
            }
            return null;
        }

        public void startConv(Player sender) {
            factory.withFirstPrompt(new ChatHandler()).withEscapeSequence("exit").buildConversation((Conversable) sender).begin();
        }
     
  13. The annotation is never required, but advised if applicable.
    But for real, I think your misunderstanding it. A Prompt is a class, so you can return a new instance of that class.
    Here's the full code... Note that I stripped down the confirmation and instead you confirm by writing true/false. There is a way to make it just like you want it with "confirm", but let this get to work first.
    Code (Java):
    public class ItemNamePrompt extends StringPrompt {

        @NotNull
        @Override
        public String getPromptText(@NotNull ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }

        @Nullable
        @Override
        public Prompt acceptInput(@NotNull ConversationContext context, @Nullable String input) {
            //translate color codes
            //either store them in your own data structure somewhere or use ConversationContext present for every conversation
            context.getAllSessionData().put("itemname", translatedInput);
            return new ConfirmItemNamePrompt();
        }
    }
    Code (Java):
    public class ConfirmItemNamePrompt extends BooleanPrompt {
        @Nullable
        @Override
        protected Prompt acceptValidatedInput(@NotNull ConversationContext context, boolean input) {
            return input ? END_OF_CONVERSATION : new ItemNamePrompt(); //if the user said no, put them back to the beginning
        }

        @NotNull
        @Override
        public String getPromptText(@NotNull ConversationContext context) {
            return context.getSessionData("itemname").toString();
        }
    }
    Code (Java):
            ConversationFactory cf = new ConversationFactory(plugin);
            Conversation conversation = cf.thatExcludesNonPlayersWithMessage("Only players can choose item names!")
                    .withEscapeSequence("exit") //if the user types exit he leaves the conversation
                    .withFirstPrompt(new ItemNamePrompt())
                    .buildConversation(player);
            conversation.begin();
    The ConversationContext has a Map (session data). This is literally just a map, you can use it the same way you would use any map. So you can get the players response by getting the specified key ("itemName").
    Here's also a flow chart explaining what the code does. The second one goes into much more detail than the first one.
    d1.png
    d2.png
     
    • Agree Agree x 2
    • Informative Informative x 2
  14. I am always amazed by the amount of detail you put in to helping people here. +1 for you :)
     
    • Like Like x 1
  15. Yes thank you for taking your time but I am still having issues. Firstly I know how to conventionally query info from a map but this is so obfuscated that it makes working with it extremely difficult to work with. For example you said I should just call the key which wont work because maps cant have the same key which means if two people are using this at once it will break. This is why I would normally like to store the players uuid as the key and their response as the value which I cant due here due to excessive over engineering. Secondly I cant call it like a normal map either because the actual map is once again hidden which makes any type of getter useless.

    Besides from the fact theres no way to actually get the info I cant even get it to run up to the point where I would need that data. As soon as I call my startConv method it does correctly send me the prompt but as soon as I reply I get a null pointer error and my messages gets sent to the public chat instead of staying confined to the conversation. The error is on the return line here in the ConfirmItemNamePrompt class...
    Code (Text):
        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return context.getSessionData("itemname").toString();
        }

    Anyways here is the rest of my code from the two classes in case you wanted to verify something...
    Code (Text):
    public class ChatHandler extends StringPrompt {
        public ConversationFactory factory = new ConversationFactory(Enchants.getInstance());

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }

        @Nullable
        @Override
        public Prompt acceptInput(ConversationContext context, @Nullable String input) {
            context.getAllSessionData().put("itemName", input);
            return new ConfirmItemNamePrompt();
        }

        public void startConv(Player sender) {
            factory.withFirstPrompt(new ChatHandler()).withEscapeSequence("exit").buildConversation((Conversable) sender).begin();
        }

    //    public ConversationContext getResponse() {
    //        return
    //    }
    //
    //    public Map<String, String> getResponse() {
    //        return
    //    }

    }
    Code (Text):
    public class ConfirmItemNamePrompt extends BooleanPrompt {
        @Nullable
        @Override
        protected Prompt acceptValidatedInput(ConversationContext context, boolean input) {
            return input ? END_OF_CONVERSATION : new ChatHandler();
        }

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return context.getSessionData("itemname").toString();
        }

    }
     
    #15 CodingCyClone, Sep 2, 2021
    Last edited: Sep 2, 2021
  16. This method gives you the underlying map. This is just a map.
    Well, in theory your concern is valid and you could solve this by using the players UUID as the key instead of an arbitrary constant string, however you still seem to struggle with understanding conversations as a whole. Each player has it's own conversation and therefore each player has his own session data (the map).
    If you really dislike the use of the provided session data, you can always create your own constructor in each Prompt class and store data in your own structure.
    Weird... try printing out the content of the map. The key should be set, as
    puts the data into the map.

    In general your ChatHandler class looks a bit messy. It extends StringPrompt but holds a ConversationFactory which is pretty confusing. A ConversationFactory is used to create a ChatHandler, not the other way around. You should really create the factory somewhere else in your code. And also Prompts should follow the naming scheme of "MyCustomPrompt" to avoid confusion.
    Since you have
    in your code, I believe you want to attempt to use the data later. Keep in mind that this is not the way to go with Conversations and Prompts. A Prompt is designed to be used in the Conversation, not later (as "permanent" storage). Instead in that case you should pass in a class through the prompt's constructor where you store the data.
    So once the conversation has finished and the player has successfully selected an item name, something like the following could work.
    Code (Java):
    public class ItemNameStorage {

    private Map<UUID, String> itemNames = new HashMap<>();

    //getter

    }
    Note that this is just an example, this map could very well be placed somewhere else..., it's just to give you an idea.
    Code (Java):
    public class ConfirmItemNamePrompt extends BooleanPrompt {

        private final Map<UUID, String> itemNames;

        public ConfirmItemNamePrompt(Map<UUID, String> itemNames) {
            this.itemNames = itemNames;
        }
        @Nullable
        @Override
        protected Prompt acceptValidatedInput(ConversationContext context, boolean input) {
            if(input) {
                itemNames.put(((Player)context.getForWhom()).getUniqueID(), conversationContext.getSessionData("itemName").toString());
                return END_OF_CONVERSATION;
            }
            else {
                return new ChatHandler();
            }
        }

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return context.getSessionData("itemname").toString();
        }

    }
    Maybe as a rule of thumb: Use sessionData for data sharing across Prompts in a conversation and use your own data structure once the conversation has ended.
     
    • Useful Useful x 1
  17. Ok thank you this is making a lot more sense now! I think I almost have it working but I have a few more issues. I changed some of what you had because it was confusing me a bit since you created a constructor that used a map as a parameter when there was no previous usage of a map. Anyways how do I stop the players input from being echoed back to the player? Also how would I make this work in the context of the method I am working with because everything else now works but I am assuming it's checking the if statement before waiting for the players response.

    Code (Text):
    public boolean addNewName(ItemStack currentItem, Player player) {
            ChatHandler chatHandler = new ChatHandler();
            ConfirmItemNamePrompt confirmItemNamePrompt = new ConfirmItemNamePrompt();
            chatHandler.factory.withFirstPrompt(new ChatHandler()).withEscapeSequence("exit").buildConversation( player).begin();
            System.out.println(confirmItemNamePrompt.getResponse());

            if(confirmItemNamePrompt.getResponse().containsKey(player.getUniqueId())) {
                String displayName = confirmItemNamePrompt.getResponse().get(player.getUniqueId());
                //next I need to add name to item which will be easy
                return true;
            }
            return false;
        }
    Code (Text):
    public class ChatHandler extends StringPrompt {
        public ConversationFactory factory = new ConversationFactory(ClonesEnchants.getInstance());

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            return ChatColor.LIGHT_PURPLE + "What would you like to name your item?";
        }

        @Nullable
        @Override
        public Prompt acceptInput(ConversationContext context, @Nullable String input) {
            assert input != null;
            context.getAllSessionData().put("itemName", getCorrectColors(input));
            return new ConfirmItemNamePrompt();
        }

        private String getCorrectColors(String message) {
            for(int i=0; i<message.length(); i++){
                char c = message.charAt(i);
                if(c == '&'){
                    message = message.replace("" + c + message.charAt(i + 1), getColor("" + c + message.charAt(i + 1)) + "");
                }
            }
            return message;
        }

        private ChatColor getColor(String messages) {
            String messageFixed = messages.replace(ChatColor.BOLD.toString(), "");

            switch (messageFixed){
                case "&": return ChatColor.BLACK;
                case "&1": return ChatColor.DARK_BLUE;
                case "&2": return ChatColor.DARK_GREEN;
                case "&3": return ChatColor.DARK_AQUA;
                case "&4": return ChatColor.DARK_RED;
                case "&5": return ChatColor.DARK_PURPLE;
                case "&6": return ChatColor.GOLD;
                case "&7": return ChatColor.GRAY;
                case "&8": return ChatColor.DARK_GRAY;
                case "&9": return ChatColor.BLUE;

                case "&f": return ChatColor.WHITE;
                case "&d": return ChatColor.LIGHT_PURPLE;
                case "&b": return ChatColor.AQUA;
                case "&a": return ChatColor.GREEN;
                case "&e": return ChatColor.YELLOW;
                case "&c": return ChatColor.RED;

                case "&l": return ChatColor.BOLD;
                case "&n": return ChatColor.UNDERLINE;
                case "&o": return ChatColor.ITALIC;
                case "&m": return ChatColor.STRIKETHROUGH;
            }
            return null;
        }

    }
    Code (Text):
    public class ConfirmItemNamePrompt extends BooleanPrompt {
        private static final Map<UUID, String> itemNames = new HashMap<>();
     

        @NotNull
        @Override
        public String getPromptText(ConversationContext context) {
            List<String> messages = new ArrayList<>();
            messages.add(ChatColor.LIGHT_PURPLE + "Is this correct? " + context.getSessionData("itemName").toString());
            messages.add(ChatColor.LIGHT_PURPLE + "If so type \"true\" if not type \"false\"");
            return String.join("\n", messages);
        }

        @Nullable
        @Override
        protected Prompt acceptValidatedInput(ConversationContext context, boolean input) {
            if(input) {
                itemNames.put(((Player) (context.getForWhom())).getUniqueId(), context.getSessionData("itemName").toString());
                return END_OF_CONVERSATION;
            }
            else {
                return new ChatHandler();
            }
        }

        public Map<UUID, String> getResponse() {
            return itemNames;
        }

    }
     

    Attached Files:

  18. This is the better way instead of using a static map inside the class, but yours will work fine as well, just not pretty.
    The ConversationFactory has a withLocalEcho method. Pass in false there.
    You can't expect the codes following the initiation of the conversation to have an response from the conversation ready. The player can wait an arbitrary amount of time before he gives an answer and therefore the chat runs on a different thread, hence the code on the main thread keeps running resulting in an error in your code, because no response is present. What you should do instead is to put your code to run after the player has confirmed his input in the acceptedValidatedInput method.
     
    • Winner Winner x 1
  19. Oh I did not know it ran on a separate thread thank you for all the help!