summaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/org/bukkit/command/ConsoleCommandSender.java4
-rw-r--r--src/main/java/org/bukkit/conversations/BooleanPrompt.java33
-rw-r--r--src/main/java/org/bukkit/conversations/Conversable.java42
-rw-r--r--src/main/java/org/bukkit/conversations/Conversation.java213
-rw-r--r--src/main/java/org/bukkit/conversations/ConversationCanceller.java29
-rw-r--r--src/main/java/org/bukkit/conversations/ConversationContext.java62
-rw-r--r--src/main/java/org/bukkit/conversations/ConversationFactory.java170
-rw-r--r--src/main/java/org/bukkit/conversations/ConversationPrefix.java17
-rw-r--r--src/main/java/org/bukkit/conversations/ExactMatchConversationCanceller.java26
-rw-r--r--src/main/java/org/bukkit/conversations/FixedSetPrompt.java40
-rw-r--r--src/main/java/org/bukkit/conversations/InactivityConversationCanceller.java75
-rw-r--r--src/main/java/org/bukkit/conversations/MessagePrompt.java37
-rw-r--r--src/main/java/org/bukkit/conversations/NullConversationPrefix.java19
-rw-r--r--src/main/java/org/bukkit/conversations/NumericPrompt.java75
-rw-r--r--src/main/java/org/bukkit/conversations/PlayerNamePrompt.java35
-rw-r--r--src/main/java/org/bukkit/conversations/PluginNameConversationPrefix.java39
-rw-r--r--src/main/java/org/bukkit/conversations/Prompt.java36
-rw-r--r--src/main/java/org/bukkit/conversations/RegexPrompt.java27
-rw-r--r--src/main/java/org/bukkit/conversations/StringPrompt.java16
-rw-r--r--src/main/java/org/bukkit/conversations/ValidatingPrompt.java69
-rw-r--r--src/main/java/org/bukkit/entity/Player.java3
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 />