mirror of
https://github.com/PaperMC/Paper.git
synced 2025-03-10 22:05:10 +01:00
[Bleeding] Added Conversations API. Addresses BUKKIT-864
By: rmichela <deltahat@gmail.com>
This commit is contained in:
parent
fb55ed2a78
commit
2280c6be2b
@ -1,4 +1,6 @@
|
|||||||
package org.bukkit.command;
|
package org.bukkit.command;
|
||||||
|
|
||||||
public interface ConsoleCommandSender extends CommandSender {
|
import org.bukkit.conversations.Conversable;
|
||||||
|
|
||||||
|
public interface ConsoleCommandSender extends CommandSender, Conversable {
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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, ", ") + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
36
paper-api/src/main/java/org/bukkit/conversations/Prompt.java
Normal file
36
paper-api/src/main/java/org/bukkit/conversations/Prompt.java
Normal file
@ -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);
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -12,13 +12,14 @@ import org.bukkit.Note;
|
|||||||
import org.bukkit.OfflinePlayer;
|
import org.bukkit.OfflinePlayer;
|
||||||
import org.bukkit.Statistic;
|
import org.bukkit.Statistic;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.conversations.Conversable;
|
||||||
import org.bukkit.map.MapView;
|
import org.bukkit.map.MapView;
|
||||||
import org.bukkit.plugin.messaging.PluginMessageRecipient;
|
import org.bukkit.plugin.messaging.PluginMessageRecipient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a player, connected or not
|
* 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.
|
* Gets the "friendly" name to display of this player. This may include color.
|
||||||
* <p />
|
* <p />
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.bukkit.conversations;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public class ConversationContextTest {
|
||||||
|
@Test
|
||||||
|
public void TestFromWhom() {
|
||||||
|
Conversable conversable = new FakeConversable();
|
||||||
|
ConversationContext context = new ConversationContext(null, conversable, new HashMap<Object, Object>());
|
||||||
|
assertEquals(conversable, context.getForWhom());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestPlugin() {
|
||||||
|
Conversable conversable = new FakeConversable();
|
||||||
|
ConversationContext context = new ConversationContext(null, conversable, new HashMap<Object, Object>());
|
||||||
|
assertEquals(null, context.getPlugin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestSessionData() {
|
||||||
|
Conversable conversable = new FakeConversable();
|
||||||
|
Map session = new HashMap();
|
||||||
|
session.put("key", "value");
|
||||||
|
ConversationContext context = new ConversationContext(null, conversable, session);
|
||||||
|
assertEquals("value", context.getSessionData("key"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
package org.bukkit.conversations;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public class ConversationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBaseConversationFlow() {
|
||||||
|
FakeConversable forWhom = new FakeConversable();
|
||||||
|
Conversation conversation = new Conversation(null, forWhom, new FirstPrompt());
|
||||||
|
|
||||||
|
// Conversation not yet begun
|
||||||
|
assertNull(forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation.getForWhom(), forWhom);
|
||||||
|
assertTrue(conversation.isModal());
|
||||||
|
|
||||||
|
// Begin the conversation
|
||||||
|
conversation.begin();
|
||||||
|
assertEquals("FirstPrompt", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.begunConversation);
|
||||||
|
|
||||||
|
// Send the first input
|
||||||
|
conversation.acceptInput("FirstInput");
|
||||||
|
assertEquals("SecondPrompt", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.abandonedConverstion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConversationFactory() {
|
||||||
|
FakeConversable forWhom = new FakeConversable();
|
||||||
|
NullConversationPrefix prefix = new NullConversationPrefix();
|
||||||
|
ConversationFactory factory = new ConversationFactory(null)
|
||||||
|
.withFirstPrompt(new FirstPrompt())
|
||||||
|
.withModality(false)
|
||||||
|
.withPrefix(prefix);
|
||||||
|
Conversation conversation = factory.buildConversation(forWhom);
|
||||||
|
|
||||||
|
// Conversation not yet begun
|
||||||
|
assertNull(forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation.getForWhom(), forWhom);
|
||||||
|
assertFalse(conversation.isModal());
|
||||||
|
assertEquals(conversation.getPrefix(), prefix);
|
||||||
|
|
||||||
|
// Begin the conversation
|
||||||
|
conversation.begin();
|
||||||
|
assertEquals("FirstPrompt", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.begunConversation);
|
||||||
|
|
||||||
|
// Send the first input
|
||||||
|
conversation.acceptInput("FirstInput");
|
||||||
|
assertEquals("SecondPrompt", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.abandonedConverstion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEscapeSequence() {
|
||||||
|
FakeConversable forWhom = new FakeConversable();
|
||||||
|
Conversation conversation = new Conversation(null, forWhom, new FirstPrompt());
|
||||||
|
conversation.addConversationCanceller(new ExactMatchConversationCanceller("bananas"));
|
||||||
|
|
||||||
|
// Begin the conversation
|
||||||
|
conversation.begin();
|
||||||
|
assertEquals("FirstPrompt", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.begunConversation);
|
||||||
|
|
||||||
|
// Send the first input
|
||||||
|
conversation.acceptInput("bananas");
|
||||||
|
assertEquals("bananas", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.abandonedConverstion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotPlayer() {
|
||||||
|
FakeConversable forWhom = new FakeConversable();
|
||||||
|
NullConversationPrefix prefix = new NullConversationPrefix();
|
||||||
|
ConversationFactory factory = new ConversationFactory(null)
|
||||||
|
.thatExcludesNonPlayersWithMessage("bye");
|
||||||
|
Conversation conversation = factory.buildConversation(forWhom);
|
||||||
|
|
||||||
|
// Begin the conversation
|
||||||
|
conversation.begin();
|
||||||
|
assertEquals("bye", forWhom.lastSentMessage);
|
||||||
|
assertEquals(conversation, forWhom.begunConversation);
|
||||||
|
assertEquals(conversation, forWhom.abandonedConverstion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FirstPrompt extends StringPrompt {
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
return "FirstPrompt";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Prompt acceptInput(ConversationContext context, String input) {
|
||||||
|
assertEquals("FirstInput", input);
|
||||||
|
context.setSessionData("data", 10);
|
||||||
|
return new SecondPrompt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SecondPrompt extends MessagePrompt {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Prompt getNextPrompt(ConversationContext context) {
|
||||||
|
return Prompt.END_OF_CONVERSATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
// Assert that session data passes from one prompt to the next
|
||||||
|
assertEquals(context.getSessionData("data"), 10);
|
||||||
|
return "SecondPrompt";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package org.bukkit.conversations;
|
||||||
|
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.permissions.Permission;
|
||||||
|
import org.bukkit.permissions.PermissionAttachment;
|
||||||
|
import org.bukkit.permissions.PermissionAttachmentInfo;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public class FakeConversable implements Conversable {
|
||||||
|
public String lastSentMessage;
|
||||||
|
public Conversation begunConversation;
|
||||||
|
public Conversation abandonedConverstion;
|
||||||
|
|
||||||
|
public boolean isConversing() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptConversationInput(String input) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean beginConversation(Conversation conversation) {
|
||||||
|
begunConversation = conversation;
|
||||||
|
conversation.outputNextPrompt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abandonConversation(Conversation conversation) {
|
||||||
|
abandonedConverstion = conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendRawMessage(String message) {
|
||||||
|
lastSentMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server getServer() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPermissionSet(String name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPermissionSet(Permission perm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPermission(String name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPermission(Permission perm) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionAttachment addAttachment(Plugin plugin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeAttachment(PermissionAttachment attachment) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recalculatePermissions() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PermissionAttachmentInfo> getEffectivePermissions() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOp() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOp(boolean value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
package org.bukkit.conversations;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public class ValidatingPromptTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestBooleanPrompt() {
|
||||||
|
TestBooleanPrompt prompt = new TestBooleanPrompt();
|
||||||
|
assertTrue(prompt.isInputValid(null, "true"));
|
||||||
|
assertFalse(prompt.isInputValid(null, "bananas"));
|
||||||
|
prompt.acceptInput(null, "true");
|
||||||
|
assertTrue(prompt.result);
|
||||||
|
prompt.acceptInput(null, "no");
|
||||||
|
assertFalse(prompt.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestFixedSetPrompt() {
|
||||||
|
TestFixedSetPrompt prompt = new TestFixedSetPrompt("foo", "bar");
|
||||||
|
assertTrue(prompt.isInputValid(null, "foo"));
|
||||||
|
assertFalse(prompt.isInputValid(null, "cheese"));
|
||||||
|
prompt.acceptInput(null, "foo");
|
||||||
|
assertEquals("foo", prompt.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestNumericPrompt() {
|
||||||
|
TestNumericPrompt prompt = new TestNumericPrompt();
|
||||||
|
assertTrue(prompt.isInputValid(null, "1010220"));
|
||||||
|
assertFalse(prompt.isInputValid(null, "tomato"));
|
||||||
|
prompt.acceptInput(null, "1010220");
|
||||||
|
assertEquals(1010220, prompt.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void TestRegexPrompt() {
|
||||||
|
TestRegexPrompt prompt = new TestRegexPrompt("a.c");
|
||||||
|
assertTrue(prompt.isInputValid(null, "abc"));
|
||||||
|
assertTrue(prompt.isInputValid(null, "axc"));
|
||||||
|
assertFalse(prompt.isInputValid(null, "xyz"));
|
||||||
|
prompt.acceptInput(null, "abc");
|
||||||
|
assertEquals("abc", prompt.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: TestPlayerNamePrompt()
|
||||||
|
|
||||||
|
private class TestBooleanPrompt extends BooleanPrompt {
|
||||||
|
public boolean result;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Prompt acceptValidatedInput(ConversationContext context, boolean input) {
|
||||||
|
result = input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestFixedSetPrompt extends FixedSetPrompt {
|
||||||
|
public String result;
|
||||||
|
|
||||||
|
public TestFixedSetPrompt(String... fixedSet) {
|
||||||
|
super(fixedSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Prompt acceptValidatedInput(ConversationContext context, String input) {
|
||||||
|
result = input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestNumericPrompt extends NumericPrompt {
|
||||||
|
public Number result;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Prompt acceptValidatedInput(ConversationContext context, Number input) {
|
||||||
|
result = input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRegexPrompt extends RegexPrompt {
|
||||||
|
public String result;
|
||||||
|
|
||||||
|
public TestRegexPrompt(String pattern) {
|
||||||
|
super(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Prompt acceptValidatedInput(ConversationContext context, String input) {
|
||||||
|
result = input;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPromptText(ConversationContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.conversations.Conversation;
|
||||||
import org.bukkit.entity.Arrow;
|
import org.bukkit.entity.Arrow;
|
||||||
import org.bukkit.entity.Egg;
|
import org.bukkit.entity.Egg;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
@ -718,4 +719,20 @@ public class TestPlayer implements Player {
|
|||||||
public boolean setWindowProperty(Property prop, int value) {
|
public boolean setWindowProperty(Property prop, int value) {
|
||||||
throw new UnsupportedOperationException("Not supported yet.");
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isConversing() {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptConversationInput(String input) {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean beginConversation(Conversation conversation) {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void abandonConversation(Conversation conversation) {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user