Scanner-like util for the player chat

Discussion in 'Plugin Development' started by Tecno_Wizard, Oct 19, 2016.

Thread Status:
Not open for further replies.
  1. Offline

    Tecno_Wizard

    Hey everyone.

    For the longest time I have been really annoyed with how repetitive it is to use the chat events to listen for inputs and decided I wanted to change that, so I am currently making this: A Scanner-like class that runs on the input of the player's chat, and I wanted to hear some opinions on it.

    Note that this is still 100% a work in progress and will not work properly in a production environment.

    Code:
    package com.ethanzeigler.utils;
    
    import org.bukkit.Bukkit;
    import org.bukkit.ChatColor;
    import org.bukkit.entity.Player;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.player.AsyncPlayerChatEvent;
    import org.bukkit.event.player.PlayerQuitEvent;
    import org.bukkit.plugin.java.JavaPlugin;
    
    import java.io.IOException;
    import java.lang.ref.WeakReference;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
    * Created by Ethan Zeigler (A.K.A Tecno_Wizard)
    * Creates a commandline like atmosphere for a player which allows the developer to read from the
    * chat box in an asynchronous manner. Players do not receive any messages from the server while open.
    */
    public class ChatCommandLineManager {
        private List<ChatCommandLine> listeners = new ArrayList<>();
        private Listener listener;
        private JavaPlugin plugin;
        private static String prefix = ChatColor.GOLD + "" + ChatColor.ITALIC + "" + ChatColor.BOLD + ">" + ChatColor.RESET + " ";
    
    //FIXME make singleton
        /**
         * Manages a group of CommandLines.
         * @param plugin Main class of Bukkit plugin
         * @param waitingMessage Message to display when a player sends a message while the command line is running but
         *                       there is nothing awaiting input at that moment.
         */
        public ChatCommandLineManager(JavaPlugin plugin, final String waitingMessage) {
            this.plugin = plugin;
            listener = new Listener() {
                @EventHandler
                public void onChat(AsyncPlayerChatEvent e) {
                    System.out.println("Chat fired");
                    ChatCommandLine senderCommandLine = null;
                    for (ChatCommandLine commandLine : listeners) {
                        if(commandLine == null) listeners.remove(commandLine);
                        if (commandLine.isOpen()) {
                            System.out.println("FOUND OPEN LINE");
                            // sender has an open commandline
                            if (commandLine.getPlayer().get().equals(e.getPlayer()) && !e.getMessage().contains(prefix)) {
                                System.out.println("IS VALID STRING");
                                if (commandLine.isActivelyReading) {
                                    System.out.println("IS READING");
    
                                    commandLine.isActivelyReading = false;
                                    commandLine.latestRawInput = e.getMessage();
                                    e.setCancelled(true);
                                    synchronized (commandLine) {
                                        commandLine.notifyAll();
                                        System.out.println("RELEASED");
                                    }
                                    break;
                                } else {
                                    e.getPlayer().sendMessage(prefix + waitingMessage);
                                    e.setCancelled(true);
                                }
    
                                // cancel recipients if the sender does not have an open line
                            } else if (e.getRecipients().contains(commandLine.getPlayer())) {
                                e.getRecipients().remove(commandLine.getPlayer());
                                break;
                            }
                        }
                    }
                }
    
                @EventHandler
                public void onPlayerQuit(PlayerQuitEvent e) {
                    for(ChatCommandLine commandLine: listeners) {
                        if(e.getPlayer().equals(commandLine.getPlayer())) {
                            commandLine.notifyAll();
                        }
                    }
                }
            };
    
            plugin.getServer().getPluginManager().registerEvents(listener, plugin);
        }
    
        /**
         * Adds a new ChatCommandLine to the manager.
         * @param player Player to open ChatCommandLine on
         * @param openingMessage Message to send to the player to signify that a ChatCommandLine has been opened on him/her
         * @return a new ChatCommandLine object
         * @throws IOException if the player already has a ChatCommandLine open or the player
         */
        public ChatCommandLine createNewChatCommandLine(Player player, String openingMessage) throws IOException, PlayerIsOfflineException {
            ChatCommandLine line = new ChatCommandLine(player, openingMessage);
            listeners.add(line);
            return line;
        }
    
        public void closeAllChatCommandLines(String closeMessage) {
            for(ChatCommandLine commandLine: listeners) {
                commandLine.notifyAll();
                commandLine.close(closeMessage);
            }
            AsyncPlayerChatEvent.getHandlerList().unregister(listener);
            PlayerQuitEvent.getHandlerList().unregister(listener);
        }
    
        public class ChatCommandLine {
            private WeakReference<Player> player;
            private String openingMessage;
            private boolean isActivelyReading = false;
            private boolean isOpen = false;
            private String latestRawInput = null;
    
            private ChatCommandLine(Player player, String openingMessage) throws IOException, PlayerIsOfflineException {
                for(ChatCommandLine commandLine: listeners) {
                    if(player.equals(commandLine.getPlayer().get())) throw new IOException("There is another ChatCommandLine open on this player.");
                }
                this.player = new WeakReference<Player>(player);
                this.openingMessage = openingMessage;
            }
    
            public Player getPlayer() {
                if(player.get().equals(null)) {
                    isActivelyReading = false;
                    synchronized (this) {
                        this.notifyAll();
                    }
                }
                return player.get();
            }
    
            public String getOpeningMessage() {
                return openingMessage;
            }
    
            public boolean isActivelyReading() {
                return isActivelyReading;
            }
    
            private void input(String input) {
                latestRawInput = input;
            }
    
            public String readLine() throws InterruptedException, IOException {
                if(this.isActivelyReading) throw new IOException("A thread is already monitoring this CommandLine");
                if(!this.isOpen) throw new IOException("ChatCommandLine is closed");
                if(this.player.get() == null) throw new IOException("The Player in question has logged off");
                synchronized (this) {
                    this.isActivelyReading = true;
                    this.wait();
                }
                if(latestRawInput == null) throw new InterruptedException("The Player in question has logged off");
                String output = latestRawInput;
                latestRawInput = null;
                return output;
            }
    
            public void close(String closingMessage) {
                this.isOpen = false;
                if(player.get() != null) player.get().sendMessage(prefix + closingMessage);
                listeners.remove(this);
            }
    
            public void open() throws IOException {
                if(player.get() == null) throw new IOException("Player is offline");
                this.isOpen = true;
                if(!listeners.contains(this)) {
                    listeners.add(this);
                    player.get().sendMessage(prefix + openingMessage);
                }
            }
    
            public boolean isOpen() {
                return isOpen;
            }
    
        public class PlayerIsOfflineException extends Exception {}
    }
    
    This (must be used async) will allow you to prompt a user for an input and read that input in what I'll call a "one straight block of code" manner.

    Does anyone have any suggestions for what I can do to make this more useful? I plan on adding readInt and other input methods as well as printing to the "console".

    Currently, if the player logs out or another operation stops the flow, null is returned or an exception is thrown. I still need to clean up for it to be one way or the other and that's partially what i'm asking about.
     
  2. Offline

    timtower Administrator Administrator Moderator

  3. Offline

    mythbusterma

    @Tecno_Wizard

    Your code isn't thread safe. That's the biggest issue I'm seeing.

    Also, you should look at the conversations API and use proper variable names.
     
  4. Offline

    PhantomUnicorns

  5. Offline

    mythbusterma

    @PhantomUnicorns

    Sorry, meant method names.

    Also, you should try and clean up that method with all the indentation.
     
  6. Offline

    Tecno_Wizard

    It's a work in progress and needs a lot of cleaning up still, I know. I'm very new to this kind of multithreading too, so I'm not surprised that there are thread safety issues. Are you speaking about how it pauses the current thread or is there something else I'm missing?

    Also, this convo API is news to me. I had no idea it even existed.
     
  7. Offline

    A5H73Y

    Coversation API
    Really helpful, I use it for conversations rather than a really long command.
     
  8. Offline

    mythbusterma

    @Tecno_Wizard

    You're sharing data across threads without any means of synchronisation. Either use PlayerChatEvent or I'll actually have to take a look and think about how to fix it.
     
  9. Offline

    Tecno_Wizard

    @mythbusterma well, the conversation API is pretty much what I was trying to make anyway, so this is useless now. Lol.

    Amazing I didn't know about this.
     
Thread Status:
Not open for further replies.

Share This Page