diff options
Diffstat (limited to 'src/main/java')
21 files changed, 1065 insertions, 2 deletions
diff --git a/src/main/java/org/bukkit/command/ConsoleCommandSender.java b/src/main/java/org/bukkit/command/ConsoleCommandSender.java index baf80b6e..f309c2ed 100644 --- a/src/main/java/org/bukkit/command/ConsoleCommandSender.java +++ b/src/main/java/org/bukkit/command/ConsoleCommandSender.java @@ -1,4 +1,6 @@ package org.bukkit.command; -public interface ConsoleCommandSender extends CommandSender { +import org.bukkit.conversations.Conversable; + +public interface ConsoleCommandSender extends CommandSender, Conversable { } diff --git a/src/main/java/org/bukkit/conversations/BooleanPrompt.java b/src/main/java/org/bukkit/conversations/BooleanPrompt.java new file mode 100644 index 00000000..3bfd733f --- /dev/null +++ b/src/main/java/org/bukkit/conversations/BooleanPrompt.java @@ -0,0 +1,33 @@ +package org.bukkit.conversations; + +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang.BooleanUtils; + +/** + * BooleanPrompt is the base class for any prompt that requires a boolean response from the user. + */ +public abstract class BooleanPrompt extends ValidatingPrompt{ + + public BooleanPrompt() { + super(); + } + + @Override + protected boolean isInputValid(ConversationContext context, String input) { + String[] accepted = {"true", "false", "on", "off", "yes", "no"}; + return ArrayUtils.contains(accepted, input.toLowerCase()); + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, String input) { + return acceptValidatedInput(context, BooleanUtils.toBoolean(input)); + } + + /** + * Override this method to perform some action with the user's boolean response. + * @param context Context information about the conversation. + * @param input The user's boolean response. + * @return The next {@link Prompt} in the prompt graph. + */ + protected abstract Prompt acceptValidatedInput(ConversationContext context, boolean input); +} diff --git a/src/main/java/org/bukkit/conversations/Conversable.java b/src/main/java/org/bukkit/conversations/Conversable.java new file mode 100644 index 00000000..0633c1d9 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/Conversable.java @@ -0,0 +1,42 @@ +package org.bukkit.conversations; + +import org.bukkit.command.CommandSender; + +/** + * The Conversable interface is used to indicate objects that can have conversations. + */ +public interface Conversable { + + /** + * Tests to see of a Conversable object is actively engaged in a conversation. + * @return True if a conversation is in progress + */ + public boolean isConversing(); + + /** + * Accepts input into the active conversation. If no conversation is in progress, + * this method does nothing. + * @param input The input message into the conversation + */ + public void acceptConversationInput(String input); + + /** + * Enters into a dialog with a Conversation object. + * @param conversation The conversation to begin + * @return True if the conversation should proceed, false if it has been enqueued + */ + public boolean beginConversation(Conversation conversation); + + /** + * Abandons an active conversation. + * @param conversation The conversation to abandon + */ + public void abandonConversation(Conversation conversation); + + /** + * Sends this sender a message raw + * + * @param message Message to be displayed + */ + public void sendRawMessage(String message); +} diff --git a/src/main/java/org/bukkit/conversations/Conversation.java b/src/main/java/org/bukkit/conversations/Conversation.java new file mode 100644 index 00000000..6a202418 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/Conversation.java @@ -0,0 +1,213 @@ +package org.bukkit.conversations; + +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The Conversation class is responsible for tracking the current state of a conversation, displaying prompts to + * the user, and dispatching the user's response to the appropriate place. Conversation objects are not typically + * instantiated directly. Instead a {@link ConversationFactory} is used to construct identical conversations on demand. + * + * Conversation flow consists of a directed graph of {@link Prompt} objects. Each time a prompt gets input from the + * user, it must return the next prompt in the graph. Since each Prompt chooses the next Prompt, complex conversation + * trees can be implemented where the nature of the player's response directs the flow of the conversation. + * + * Each conversation has a {@link ConversationPrefix} that prepends all output from the conversation to the player. + * The ConversationPrefix can be used to display the plugin name or conversation status as the conversation evolves. + * + * Each conversation has a timeout measured in the number of inactive seconds to wait before abandoning the conversation. + * If the inactivity timeout is reached, the conversation is abandoned and the user's incoming and outgoing chat is + * returned to normal. + * + * You should not construct a conversation manually. Instead, use the {@link ConversationFactory} for access to all + * available options. + */ +public class Conversation { + + private Prompt firstPrompt; + private boolean abandoned; + protected Prompt currentPrompt; + protected ConversationContext context; + protected boolean modal; + protected ConversationPrefix prefix; + protected List<ConversationCanceller> cancellers; + + /** + * Initializes a new Conversation. + * @param plugin The plugin that owns this conversation. + * @param forWhom The entity for whom this conversation is mediating. + * @param firstPrompt The first prompt in the conversation graph. + */ + public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt) { + this(plugin, forWhom, firstPrompt, new HashMap<Object, Object>()); + } + + /** + * Initializes a new Conversation. + * @param plugin The plugin that owns this conversation. + * @param forWhom The entity for whom this conversation is mediating. + * @param firstPrompt The first prompt in the conversation graph. + * @param initialSessionData Any initial values to put in the conversation context sessionData map. + */ + public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt, Map<Object, Object> initialSessionData) { + this.firstPrompt = firstPrompt; + this.context = new ConversationContext(plugin, forWhom, initialSessionData); + this.modal = true; + this.prefix = new NullConversationPrefix(); + this.cancellers = new ArrayList<ConversationCanceller>(); + } + + /** + * Gets the entity for whom this conversation is mediating. + * @return The entity. + */ + public Conversable getForWhom() { + return context.getForWhom(); + } + + /** + * Gets the modality of this conversation. If a conversation is modal, all messages directed to the player + * are suppressed for the duration of the conversation. + * @return The conversation modality. + */ + public boolean isModal() { + return modal; + } + + /** + * Sets the modality of this conversation. If a conversation is modal, all messages directed to the player + * are suppressed for the duration of the conversation. + * @param modal The new conversation modality. + */ + void setModal(boolean modal) { + this.modal = modal; + } + + /** + * Gets the {@link ConversationPrefix} that prepends all output from this conversation. + * @return The ConversationPrefix in use. + */ + public ConversationPrefix getPrefix() { + return prefix; + } + + /** + * Sets the {@link ConversationPrefix} that prepends all output from this conversation. + * @param prefix The ConversationPrefix to use. + */ + void setPrefix(ConversationPrefix prefix) { + this.prefix = prefix; + } + + /** + * Adds a {@link ConversationCanceller} to the cancellers collection. + * @param canceller The {@link ConversationCanceller} to add. + */ + void addConversationCanceller(ConversationCanceller canceller) { + canceller.setConversation(this); + this.cancellers.add(canceller); + } + + /** + * Gets the list of {@link ConversationCanceller}s + * @return The list. + */ + public List<ConversationCanceller> getCancellers() { + return cancellers; + } + + /** + * Returns the Conversation's {@link ConversationContext}. + * @return The ConversationContext. + */ + public ConversationContext getContext() { + return context; + } + + /** + * Displays the first prompt of this conversation and begins redirecting the user's chat responses. + */ + public void begin() { + if (currentPrompt == null) { + abandoned = false; + currentPrompt = firstPrompt; + context.getForWhom().beginConversation(this); + } + } + + /** + * Returns Returns the current state of the conversation. + * @return The current state of the conversation. + */ + public ConversationState getState() { + if (currentPrompt != null) { + return ConversationState.STARTED; + } else if (abandoned) { + return ConversationState.ABANDONED; + } else { + return ConversationState.UNSTARTED; + } + } + + /** + * Passes player input into the current prompt. The next prompt (as determined by the current prompt) is then + * displayed to the user. + * @param input The user's chat text. + */ + public void acceptInput(String input) { + if (currentPrompt != null) { + + // Echo the user's input + context.getForWhom().sendRawMessage(prefix.getPrefix(context) + input); + + // Test for conversation abandonment based on input + for(ConversationCanceller canceller : cancellers) { + if (canceller.cancelBasedOnInput(context, input)) { + abandon(); + return; + } + } + + // Not abandoned, output the next prompt + currentPrompt = currentPrompt.acceptInput(context, input); + outputNextPrompt(); + } + } + + /** + * Abandons and resets the current conversation. Restores the user's normal chat behavior. + */ + public void abandon() { + if (!abandoned) { + abandoned = true; + currentPrompt = null; + context.getForWhom().abandonConversation(this); + } + } + + /** + * Displays the next user prompt and abandons the conversation if the next prompt is null. + */ + public void outputNextPrompt() { + if (currentPrompt == null) { + abandon(); + } else { + context.getForWhom().sendRawMessage(prefix.getPrefix(context) + currentPrompt.getPromptText(context)); + if (!currentPrompt.blocksForInput(context)) { + currentPrompt = currentPrompt.acceptInput(context, null); + outputNextPrompt(); + } + } + } + + public enum ConversationState { + UNSTARTED, + STARTED, + ABANDONED + } +} diff --git a/src/main/java/org/bukkit/conversations/ConversationCanceller.java b/src/main/java/org/bukkit/conversations/ConversationCanceller.java new file mode 100644 index 00000000..b2d91f2b --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ConversationCanceller.java @@ -0,0 +1,29 @@ +package org.bukkit.conversations; + +/** + * A ConversationCanceller is a class that cancels an active {@link Conversation}. A Conversation can have more + * than one ConversationCanceller. + */ +public interface ConversationCanceller extends Cloneable { + + /** + * Sets the conversation this ConversationCanceller can optionally cancel. + * @param conversation A conversation. + */ + public void setConversation(Conversation conversation); + + /** + * Cancels a conversation based on user input/ + * @param context Context information about the conversation. + * @param input The input text from the user. + * @return True to cancel the conversation, False otherwise. + */ + public boolean cancelBasedOnInput(ConversationContext context, String input); + + /** + * Allows the {@link ConversationFactory} to duplicate this ConversationCanceller when creating a new {@link Conversation}. + * Implementing this method should reset any internal object state. + * @return A clone. + */ + public ConversationCanceller clone(); +} diff --git a/src/main/java/org/bukkit/conversations/ConversationContext.java b/src/main/java/org/bukkit/conversations/ConversationContext.java new file mode 100644 index 00000000..65bcf981 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ConversationContext.java @@ -0,0 +1,62 @@ +package org.bukkit.conversations; + +import org.bukkit.plugin.Plugin; + +import java.util.Map; + +/** + * A ConversationContext provides continuity between nodes in the prompt graph by giving the developer access to the + * subject of the conversation and a generic map for storing values that are shared between all {@link Prompt} + * invocations. + */ +public class ConversationContext { + private Conversable forWhom; + private Map<Object, Object> sessionData; + private Plugin plugin; + + /** + * @param forWhom The subject of the conversation. + * @param initialSessionData Any initial values to put in the sessionData map. + */ + public ConversationContext(Plugin plugin, Conversable forWhom, Map<Object, Object> initialSessionData) { + this.plugin = plugin; + this.forWhom = forWhom; + this.sessionData = initialSessionData; + } + + /** + * Gets the plugin that owns this conversation. + * @return The owning plugin. + */ + public Plugin getPlugin() { + return plugin; + } + + /** + * Gets the subject of the conversation. + * @return The subject of the conversation. + */ + public Conversable getForWhom() { + return forWhom; + } + + /** + * Gets session data shared between all {@link Prompt} invocations. Use this as a way + * to pass data through each Prompt as the conversation develops. + * @param key The session data key. + * @return The requested session data. + */ + public Object getSessionData(Object key) { + return sessionData.get(key); + } + + /** + * Sets session data shared between all {@link Prompt} invocations. Use this as a way to pass + * data through each prompt as the conversation develops. + * @param key The session data key. + * @param value The session data value. + */ + public void setSessionData(Object key, Object value) { + sessionData.put(key, value); + } +} diff --git a/src/main/java/org/bukkit/conversations/ConversationFactory.java b/src/main/java/org/bukkit/conversations/ConversationFactory.java new file mode 100644 index 00000000..308a302a --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ConversationFactory.java @@ -0,0 +1,170 @@ +package org.bukkit.conversations; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A ConversationFactory is responsible for creating a {@link Conversation} from a predefined template. A ConversationFactory + * is typically created when a plugin is instantiated and builds a Conversation each time a user initiates a conversation + * with the plugin. Each Conversation maintains its own state and calls back as needed into the plugin. + * + * The ConversationFactory implements a fluid API, allowing parameters to be set as an extension to the constructor. + */ +public class ConversationFactory { + + protected Plugin plugin; + protected boolean isModal; + protected ConversationPrefix prefix; + protected Prompt firstPrompt; + protected Map<Object, Object> initialSessionData; + protected String playerOnlyMessage; + protected List<ConversationCanceller> cancellers; + + /** + * Constructs a ConversationFactory. + */ + public ConversationFactory(Plugin plugin) + { + this.plugin = plugin; + isModal = true; + prefix = new NullConversationPrefix(); + firstPrompt = Prompt.END_OF_CONVERSATION; + initialSessionData = new HashMap<Object, Object>(); + playerOnlyMessage = null; + cancellers = new ArrayList<ConversationCanceller>(); + } + + /** + * Sets the modality of all {@link Conversation}s created by this factory. If a conversation is modal, all messages + * directed to the player are suppressed for the duration of the conversation. + * + * The default is True. + * @param modal The modality of all conversations to be created. + * @return This object. + */ + public ConversationFactory withModality(boolean modal) + { + isModal = modal; + return this; + } + + /** + * Sets the {@link ConversationPrefix} that prepends all output from all generated conversations. + * + * The default is a {@link NullConversationPrefix}; + * @param prefix The ConversationPrefix to use. + * @return This object. + */ + public ConversationFactory withPrefix(ConversationPrefix prefix) { + this.prefix = prefix; + return this; + } + + /** + * Sets the number of inactive seconds to wait before automatically abandoning all generated conversations. + * + * The default is 600 seconds (5 minutes). + * @param timeoutSeconds The number of seconds to wait. + * @return This object. + */ + public ConversationFactory withTimeout(int timeoutSeconds) { + return withConversationCanceller(new InactivityConversationCanceller(plugin, timeoutSeconds)); + } + + /** + * Sets the first prompt to use in all generated conversations. + * + * The default is Prompt.END_OF_CONVERSATION. + * @param firstPrompt The first prompt. + * @return This object. + */ + public ConversationFactory withFirstPrompt(Prompt firstPrompt) { + this.firstPrompt = firstPrompt; + return this; + } + + /** + * Sets any initial data with which to populate the conversation context sessionData map. + * @param initialSessionData The conversation context's initial sessionData. + * @return This object. + */ + public ConversationFactory withInitialSessionData(Map<Object, Object> initialSessionData) { + this.initialSessionData = initialSessionData; + return this; + } + + /** + * Sets the player input that, when received, will immediately terminate the conversation. + * @param escapeSequence Input to terminate the conversation. + * @return This object. + */ + public ConversationFactory withEscapeSequence(String escapeSequence) { + return withConversationCanceller(new ExactMatchConversationCanceller(escapeSequence)); + } + + + /** + * Adds a {@link ConversationCanceller to constructed conversations.} + * @param canceller The {@link ConversationCanceller to add.} + * @return This object. + */ + public ConversationFactory withConversationCanceller(ConversationCanceller canceller) { + cancellers.add(canceller); + return this; + } + + /** + * Prevents this factory from creating a conversation for non-player {@link Conversable} objects. + * @param playerOnlyMessage The message to return to a non-play in lieu of starting a conversation. + * @return This object. + */ + public ConversationFactory thatExcludesNonPlayersWithMessage(String playerOnlyMessage) { + this.playerOnlyMessage = playerOnlyMessage; + return this; + } + + /** + * Constructs a {@link Conversation} in accordance with the defaults set for this factory. + * @param forWhom The entity for whom the new conversation is mediating. + * @return A new conversation. + */ + public Conversation buildConversation(Conversable forWhom) { + //Abort conversation construction if we aren't supposed to talk to non-players + if(playerOnlyMessage != null && !(forWhom instanceof Player)) { + return new Conversation(plugin, forWhom, new NotPlayerMessagePrompt()); + } + + //Clone any initial session data + Map<Object, Object> copiedInitialSessionData = new HashMap<Object, Object>(); + copiedInitialSessionData.putAll(initialSessionData); + + //Build and return a conversation + Conversation conversation = new Conversation(plugin, forWhom, firstPrompt, copiedInitialSessionData); + conversation.setModal(isModal); + conversation.setPrefix(prefix); + + //Clone the conversation cancellers + for(ConversationCanceller canceller : cancellers) { + conversation.addConversationCanceller(canceller.clone()); + } + + return conversation; + } + + private class NotPlayerMessagePrompt extends MessagePrompt { + + public String getPromptText(ConversationContext context) { + return playerOnlyMessage; + } + + @Override + protected Prompt getNextPrompt(ConversationContext context) { + return Prompt.END_OF_CONVERSATION; + } + } +} diff --git a/src/main/java/org/bukkit/conversations/ConversationPrefix.java b/src/main/java/org/bukkit/conversations/ConversationPrefix.java new file mode 100644 index 00000000..73c58bbe --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ConversationPrefix.java @@ -0,0 +1,17 @@ +package org.bukkit.conversations; + +import org.bukkit.command.CommandSender; + +/** + * A ConversationPrefix implementation prepends all output from the conversation to the player. + * The ConversationPrefix can be used to display the plugin name or conversation status as the conversation evolves. + */ +public interface ConversationPrefix { + + /** + * Gets the prefix to use before each message to the player. + * @param context Context information about the conversation. + * @return The prefix text. + */ + String getPrefix(ConversationContext context); +} diff --git a/src/main/java/org/bukkit/conversations/ExactMatchConversationCanceller.java b/src/main/java/org/bukkit/conversations/ExactMatchConversationCanceller.java new file mode 100644 index 00000000..ad387534 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ExactMatchConversationCanceller.java @@ -0,0 +1,26 @@ +package org.bukkit.conversations; + +/** + * An ExactMatchConversationCanceller cancels a conversation if the user enters an exact input string + */ +public class ExactMatchConversationCanceller implements ConversationCanceller { + private String escapeSequence; + + /** + * Builds an ExactMatchConversationCanceller. + * @param escapeSequence The string that, if entered by the user, will cancel the conversation. + */ + public ExactMatchConversationCanceller(String escapeSequence) { + this.escapeSequence = escapeSequence; + } + + public void setConversation(Conversation conversation) {} + + public boolean cancelBasedOnInput(ConversationContext context, String input) { + return input.equals(escapeSequence); + } + + public ConversationCanceller clone() { + return new ExactMatchConversationCanceller(escapeSequence); + } +} diff --git a/src/main/java/org/bukkit/conversations/FixedSetPrompt.java b/src/main/java/org/bukkit/conversations/FixedSetPrompt.java new file mode 100644 index 00000000..189fda9e --- /dev/null +++ b/src/main/java/org/bukkit/conversations/FixedSetPrompt.java @@ -0,0 +1,40 @@ +package org.bukkit.conversations; + +import org.apache.commons.lang.StringUtils; + +import java.util.Arrays; +import java.util.List; + +/** + * FixedSetPrompt is the base class for any prompt that requires a fixed set response from the user. + */ +public abstract class FixedSetPrompt extends ValidatingPrompt { + + protected List<String> fixedSet; + + /** + * Creates a FixedSetPrompt from a set of strings. + * foo = new FixedSetPrompt("bar", "cheese", "panda"); + * @param fixedSet A fixed set of strings, one of which the user must type. + */ + public FixedSetPrompt(String... fixedSet) { + super(); + this.fixedSet = Arrays.asList(fixedSet); + } + + private FixedSetPrompt() {} + + @Override + protected boolean isInputValid(ConversationContext context, String input) { + return fixedSet.contains(input); + } + + /** + * Utility function to create a formatted string containing all the options declared in the constructor. + * The result is formatted like "[bar, cheese, panda]" + * @return + */ + protected String formatFixedSet() { + return "[" + StringUtils.join(fixedSet, ", ") + "]"; + } +} diff --git a/src/main/java/org/bukkit/conversations/InactivityConversationCanceller.java b/src/main/java/org/bukkit/conversations/InactivityConversationCanceller.java new file mode 100644 index 00000000..ed0ec952 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/InactivityConversationCanceller.java @@ -0,0 +1,75 @@ +package org.bukkit.conversations; + +import org.bukkit.Server; +import org.bukkit.plugin.Plugin; + +/** + * An InactivityConversationCanceller will cancel a {@link Conversation} after a period of inactivity by the user. + */ +public class InactivityConversationCanceller implements ConversationCanceller { + protected Plugin plugin; + protected int timeoutSeconds; + protected Conversation conversation; + private int taskId = -1; + + /** + * Creates an InactivityConversationCanceller. + * @param plugin The owning plugin. + * @param timeoutSeconds The number of seconds of inactivity to wait. + */ + public InactivityConversationCanceller(Plugin plugin, int timeoutSeconds) { + this.plugin = plugin; + this.timeoutSeconds = timeoutSeconds; + } + + public void setConversation(Conversation conversation) { + this.conversation = conversation; + startTimer(); + } + + public boolean cancelBasedOnInput(ConversationContext context, String input) { + // Reset the inactivity timer + stopTimer(); + startTimer(); + return false; + } + + public ConversationCanceller clone() { + return new InactivityConversationCanceller(plugin, timeoutSeconds); + } + + /** + * Starts an inactivity timer. + */ + private void startTimer() { + taskId = plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, new Runnable() { + public void run() { + if (conversation.getState() == Conversation.ConversationState.UNSTARTED) { + startTimer(); + } else if (conversation.getState() == Conversation.ConversationState.STARTED) { + cancelling(conversation); + conversation.abandon(); + } + } + }, timeoutSeconds * 20); + } + + /** + * Stops the active inactivity timer. + */ + private void stopTimer() { + if (taskId != -1) { + plugin.getServer().getScheduler().cancelTask(taskId); + taskId = -1; + } + } + + /** + * Subclasses of InactivityConversationCanceller can override this method to take additional actions when the + * inactivity timer abandons the conversation. + * @param conversation The conversation being abandoned. + */ + protected void cancelling(Conversation conversation) { + + } +} diff --git a/src/main/java/org/bukkit/conversations/MessagePrompt.java b/src/main/java/org/bukkit/conversations/MessagePrompt.java new file mode 100644 index 00000000..9eb52131 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/MessagePrompt.java @@ -0,0 +1,37 @@ +package org.bukkit.conversations; + +/** + * MessagePrompt is the base class for any prompt that only displays a message to the user and requires no input. + */ +public abstract class MessagePrompt implements Prompt{ + + public MessagePrompt() { + super(); + } + + /** + * Message prompts never wait for user input before continuing. + * @param context Context information about the conversation. + * @return + */ + public boolean blocksForInput(ConversationContext context) { + return false; + } + + /** + * Accepts and ignores any user input, returning the next prompt in the prompt graph instead. + * @param context Context information about the conversation. + * @param input Ignored. + * @return The next prompt in the prompt graph. + */ + public Prompt acceptInput(ConversationContext context, String input) { + return getNextPrompt(context); + } + + /** + * Override this method to return the next prompt in the prompt graph. + * @param context Context information about the conversation. + * @return The next prompt in the prompt graph. + */ + protected abstract Prompt getNextPrompt(ConversationContext context); +} diff --git a/src/main/java/org/bukkit/conversations/NullConversationPrefix.java b/src/main/java/org/bukkit/conversations/NullConversationPrefix.java new file mode 100644 index 00000000..9e94d205 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/NullConversationPrefix.java @@ -0,0 +1,19 @@ +package org.bukkit.conversations; + +import org.bukkit.command.CommandSender; + +/** + * NullConversationPrefix is a {@link ConversationPrefix} implementation that displays nothing in front of + * conversation output. + */ +public class NullConversationPrefix implements ConversationPrefix{ + + /** + * Prepends each conversation message with an empty string. + * @param context Context information about the conversation. + * @return An empty string. + */ + public String getPrefix(ConversationContext context) { + return ""; + } +} diff --git a/src/main/java/org/bukkit/conversations/NumericPrompt.java b/src/main/java/org/bukkit/conversations/NumericPrompt.java new file mode 100644 index 00000000..dcff4ad5 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/NumericPrompt.java @@ -0,0 +1,75 @@ +package org.bukkit.conversations; + +import org.apache.commons.lang.math.NumberUtils; + +/** + * NumericPrompt is the base class for any prompt that requires a {@link Number} response from the user. + */ +public abstract class NumericPrompt extends ValidatingPrompt{ + public NumericPrompt() { + super(); + } + + @Override + protected boolean isInputValid(ConversationContext context, String input) { + return NumberUtils.isNumber(input) && isNumberValid(context, NumberUtils.createNumber(input)); + } + + /** + * Override this method to do further validation on the numeric player input after the input has been determined + * to actually be a number. + * @param context Context information about the conversation. + * @param input The number the player provided. + * @return The validity of the player's input. + */ + protected boolean isNumberValid(ConversationContext context, Number input) { + return true; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, String input) { + try + { + return acceptValidatedInput(context, NumberUtils.createNumber(input)); + } catch (NumberFormatException e) { + return acceptValidatedInput(context, NumberUtils.INTEGER_ZERO); + } + } + + /** + * Override this method to perform some action with the user's integer response. + * @param context Context information about the conversation. + * @param input The user's response as a {@link Number}. + * @return The next {@link Prompt} in the prompt graph. + */ + protected abstract Prompt acceptValidatedInput(ConversationContext context, Number input); + + @Override + protected String getFailedValidationText(ConversationContext context, String invalidInput) { + if (NumberUtils.isNumber(invalidInput)) { + return getFailedValidationText(context, NumberUtils.createNumber(invalidInput)); + } else { + return getInputNotNumericText(context, invalidInput); + } + } + + /** + * Optionally override this method to display an additional message if the user enters an invalid number. + * @param context Context information about the conversation. + * @param invalidInput The invalid input provided by the user. + * @return A message explaining how to correct the input. + */ + protected String getInputNotNumericText(ConversationContext context, String invalidInput) { + return null; + } + + /** + * Optionally override this method to display an additional message if the user enters an invalid numeric input. + * @param context Context information about the conversation. + * @param invalidInput The invalid input provided by the user. + * @return A message explaining how to correct the input. + */ + protected String getFailedValidationText(ConversationContext context, Number invalidInput) { + return null; + } +} diff --git a/src/main/java/org/bukkit/conversations/PlayerNamePrompt.java b/src/main/java/org/bukkit/conversations/PlayerNamePrompt.java new file mode 100644 index 00000000..bc427cfc --- /dev/null +++ b/src/main/java/org/bukkit/conversations/PlayerNamePrompt.java @@ -0,0 +1,35 @@ +package org.bukkit.conversations; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +/** + * PlayerNamePrompt is the base class for any prompt that requires the player to enter another player's name. + */ +public abstract class PlayerNamePrompt extends ValidatingPrompt{ + private Plugin plugin; + + public PlayerNamePrompt(Plugin plugin) { + super(); + this.plugin = plugin; + } + + @Override + protected boolean isInputValid(ConversationContext context, String input) { + return plugin.getServer().getPlayer(input) != null; + + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, String input) { + return acceptValidatedInput(context, plugin.getServer().getPlayer(input)); + } + + /** + * Override this method to perform some action with the user's player name response. + * @param context Context information about the conversation. + * @param input The user's player name response. + * @return The next {@link Prompt} in the prompt graph. + */ + protected abstract Prompt acceptValidatedInput(ConversationContext context, Player input); +} diff --git a/src/main/java/org/bukkit/conversations/PluginNameConversationPrefix.java b/src/main/java/org/bukkit/conversations/PluginNameConversationPrefix.java new file mode 100644 index 00000000..ec2492b1 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/PluginNameConversationPrefix.java @@ -0,0 +1,39 @@ +package org.bukkit.conversations; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; + +/** + * PluginNameConversationPrefix is a {@link ConversationPrefix} implementation that displays the plugin name in front of + * conversation output. + */ +public class PluginNameConversationPrefix implements ConversationPrefix { + + protected String separator; + protected ChatColor prefixColor; + protected Plugin plugin; + + private String cachedPrefix; + + public PluginNameConversationPrefix(Plugin plugin) { + this(plugin, " > ", ChatColor.LIGHT_PURPLE); + } + + public PluginNameConversationPrefix(Plugin plugin, String separator, ChatColor prefixColor) { + this.separator = separator; + this.prefixColor = prefixColor; + this.plugin = plugin; + + cachedPrefix = prefixColor + plugin.getDescription().getName() + separator + ChatColor.WHITE; + } + + /** + * Prepends each conversation message with the plugin name. + * @param context Context information about the conversation. + * @return An empty string. + */ + public String getPrefix(ConversationContext context) { + return cachedPrefix; + } +} diff --git a/src/main/java/org/bukkit/conversations/Prompt.java b/src/main/java/org/bukkit/conversations/Prompt.java new file mode 100644 index 00000000..1b376d8d --- /dev/null +++ b/src/main/java/org/bukkit/conversations/Prompt.java @@ -0,0 +1,36 @@ +package org.bukkit.conversations; + +/** + * A Prompt is the main constituent of a {@link Conversation}. Each prompt displays text to the user and optionally + * waits for a user's response. Prompts are chained together into a directed graph that represents the conversation + * flow. To halt a conversation, END_OF_CONVERSATION is returned in liu of another Prompt object. + */ +public interface Prompt extends Cloneable { + + /** + * A convenience constant for indicating the end of a conversation. + */ + static final Prompt END_OF_CONVERSATION = null; + + /** + * Gets the text to display to the user when this prompt is first presented. + * @param context Context information about the conversation. + * @return The text to display. + */ + String getPromptText(ConversationContext context); + + /** + * Checks to see if this prompt implementation should wait for user input or immediately display the next prompt. + * @param context Context information about the conversation. + * @return If true, the {@link Conversation} will wait for input before continuing. + */ + boolean blocksForInput(ConversationContext context); + + /** + * Accepts and processes input from the user. Using the input, the next Prompt in the prompt graph is returned. + * @param context Context information about the conversation. + * @param input The input text from the user. + * @return The next Prompt in the prompt graph. + */ + Prompt acceptInput(ConversationContext context, String input); +} diff --git a/src/main/java/org/bukkit/conversations/RegexPrompt.java b/src/main/java/org/bukkit/conversations/RegexPrompt.java new file mode 100644 index 00000000..437f9ca4 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/RegexPrompt.java @@ -0,0 +1,27 @@ +package org.bukkit.conversations; + +import java.util.regex.Pattern; + +/** + * RegexPrompt is the base class for any prompt that requires an input validated by a regular expression. + */ +public abstract class RegexPrompt extends ValidatingPrompt { + + private Pattern pattern; + + public RegexPrompt(String regex) { + this(Pattern.compile(regex)); + } + + public RegexPrompt(Pattern pattern) { + super(); + this.pattern = pattern; + } + + private RegexPrompt() {} + + @Override + protected boolean isInputValid(ConversationContext context, String input) { + return pattern.matcher(input).matches(); + } +} diff --git a/src/main/java/org/bukkit/conversations/StringPrompt.java b/src/main/java/org/bukkit/conversations/StringPrompt.java new file mode 100644 index 00000000..d297835b --- /dev/null +++ b/src/main/java/org/bukkit/conversations/StringPrompt.java @@ -0,0 +1,16 @@ +package org.bukkit.conversations; + +/** + * StringPrompt is the base class for any prompt that accepts an arbitrary string from the user. + */ +public abstract class StringPrompt implements Prompt{ + + /** + * Ensures that the prompt waits for the user to provide input. + * @param context Context information about the conversation. + * @return True. + */ + public boolean blocksForInput(ConversationContext context) { + return true; + } +} diff --git a/src/main/java/org/bukkit/conversations/ValidatingPrompt.java b/src/main/java/org/bukkit/conversations/ValidatingPrompt.java new file mode 100644 index 00000000..d6050a85 --- /dev/null +++ b/src/main/java/org/bukkit/conversations/ValidatingPrompt.java @@ -0,0 +1,69 @@ +package org.bukkit.conversations; + +import org.bukkit.ChatColor; + +/** + * ValidatingPrompt is the base class for any prompt that requires validation. ValidatingPrompt will keep replaying + * the prompt text until the user enters a valid response. + */ +public abstract class ValidatingPrompt implements Prompt { + public ValidatingPrompt() { + super(); + } + + /** + * Accepts and processes input from the user and validates it. If validation fails, this prompt is returned for + * re-execution, otherwise the next Prompt in the prompt graph is returned. + * @param context Context information about the conversation. + * @param input The input text from the user. + * @return This prompt or the next Prompt in the prompt graph. + */ + public Prompt acceptInput(ConversationContext context, String input) { + if (isInputValid(context, input)) { + return acceptValidatedInput(context, input); + } else { + String failPrompt = getFailedValidationText(context, input); + if (failPrompt != null) { + context.getForWhom().sendRawMessage(ChatColor.RED + failPrompt); + } + // Redisplay this prompt to the user to re-collect input + return this; + } + } + + /** + * Ensures that the prompt waits for the user to provide input. + * @param context Context information about the conversation. + * @return True. + */ + public boolean blocksForInput(ConversationContext context) { + return true; + } + + /** + * Override this method to check the validity of the player's input. + * @param context Context information about the conversation. + * @param input The player's raw console input. + * @return True or false depending on the validity of the input. + */ + protected abstract boolean isInputValid(ConversationContext context, String input); + + /** + * Override this method to accept and processes the validated input from the user. Using the input, the next Prompt + * in the prompt graph should be returned. + * @param context Context information about the conversation. + * @param input The validated input text from the user. + * @return The next Prompt in the prompt graph. + */ + protected abstract Prompt acceptValidatedInput(ConversationContext context, String input); + + /** + * Optionally override this method to display an additional message if the user enters an invalid input. + * @param context Context information about the conversation. + * @param invalidInput The invalid input provided by the user. + * @return A message explaining how to correct the input. + */ + protected String getFailedValidationText(ConversationContext context, String invalidInput) { + return null; + } +} diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java index 60cad93b..0d0e7f4d 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java @@ -12,13 +12,14 @@ import org.bukkit.Note; import org.bukkit.OfflinePlayer; import org.bukkit.Statistic; import org.bukkit.command.CommandSender; +import org.bukkit.conversations.Conversable; import org.bukkit.map.MapView; import org.bukkit.plugin.messaging.PluginMessageRecipient; /** * Represents a player, connected or not */ -public interface Player extends HumanEntity, CommandSender, OfflinePlayer, PluginMessageRecipient { +public interface Player extends HumanEntity, Conversable, CommandSender, OfflinePlayer, PluginMessageRecipient { /** * Gets the "friendly" name to display of this player. This may include color. * <p /> |