001 package org.bukkit.conversations; 002 003 import org.bukkit.plugin.Plugin; 004 005 import java.util.ArrayList; 006 import java.util.HashMap; 007 import java.util.List; 008 import java.util.Map; 009 010 /** 011 * The Conversation class is responsible for tracking the current state of a 012 * conversation, displaying prompts to the user, and dispatching the user's 013 * response to the appropriate place. Conversation objects are not typically 014 * instantiated directly. Instead a {@link ConversationFactory} is used to 015 * construct identical conversations on demand. 016 * <p> 017 * Conversation flow consists of a directed graph of {@link Prompt} objects. 018 * Each time a prompt gets input from the user, it must return the next prompt 019 * in the graph. Since each Prompt chooses the next Prompt, complex 020 * conversation trees can be implemented where the nature of the player's 021 * response directs the flow of the conversation. 022 * <p> 023 * Each conversation has a {@link ConversationPrefix} that prepends all output 024 * from the conversation to the player. The ConversationPrefix can be used to 025 * display the plugin name or conversation status as the conversation evolves. 026 * <p> 027 * Each conversation has a timeout measured in the number of inactive seconds 028 * to wait before abandoning the conversation. If the inactivity timeout is 029 * reached, the conversation is abandoned and the user's incoming and outgoing 030 * chat is returned to normal. 031 * <p> 032 * You should not construct a conversation manually. Instead, use the {@link 033 * ConversationFactory} for access to all available options. 034 */ 035 public class Conversation { 036 037 private Prompt firstPrompt; 038 private boolean abandoned; 039 protected Prompt currentPrompt; 040 protected ConversationContext context; 041 protected boolean modal; 042 protected boolean localEchoEnabled; 043 protected ConversationPrefix prefix; 044 protected List<ConversationCanceller> cancellers; 045 protected List<ConversationAbandonedListener> abandonedListeners; 046 047 /** 048 * Initializes a new Conversation. 049 * 050 * @param plugin The plugin that owns this conversation. 051 * @param forWhom The entity for whom this conversation is mediating. 052 * @param firstPrompt The first prompt in the conversation graph. 053 */ 054 public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt) { 055 this(plugin, forWhom, firstPrompt, new HashMap<Object, Object>()); 056 } 057 058 /** 059 * Initializes a new Conversation. 060 * 061 * @param plugin The plugin that owns this conversation. 062 * @param forWhom The entity for whom this conversation is mediating. 063 * @param firstPrompt The first prompt in the conversation graph. 064 * @param initialSessionData Any initial values to put in the conversation 065 * context sessionData map. 066 */ 067 public Conversation(Plugin plugin, Conversable forWhom, Prompt firstPrompt, Map<Object, Object> initialSessionData) { 068 this.firstPrompt = firstPrompt; 069 this.context = new ConversationContext(plugin, forWhom, initialSessionData); 070 this.modal = true; 071 this.localEchoEnabled = true; 072 this.prefix = new NullConversationPrefix(); 073 this.cancellers = new ArrayList<ConversationCanceller>(); 074 this.abandonedListeners = new ArrayList<ConversationAbandonedListener>(); 075 } 076 077 /** 078 * Gets the entity for whom this conversation is mediating. 079 * 080 * @return The entity. 081 */ 082 public Conversable getForWhom() { 083 return context.getForWhom(); 084 } 085 086 /** 087 * Gets the modality of this conversation. If a conversation is modal, all 088 * messages directed to the player are suppressed for the duration of the 089 * conversation. 090 * 091 * @return The conversation modality. 092 */ 093 public boolean isModal() { 094 return modal; 095 } 096 097 /** 098 * Sets the modality of this conversation. If a conversation is modal, 099 * all messages directed to the player are suppressed for the duration of 100 * the conversation. 101 * 102 * @param modal The new conversation modality. 103 */ 104 void setModal(boolean modal) { 105 this.modal = modal; 106 } 107 108 /** 109 * Gets the status of local echo for this conversation. If local echo is 110 * enabled, any text submitted to a conversation gets echoed back into the 111 * submitter's chat window. 112 * 113 * @return The status of local echo. 114 */ 115 public boolean isLocalEchoEnabled() { 116 return localEchoEnabled; 117 } 118 119 /** 120 * Sets the status of local echo for this conversation. If local echo is 121 * enabled, any text submitted to a conversation gets echoed back into the 122 * submitter's chat window. 123 * 124 * @param localEchoEnabled The status of local echo. 125 */ 126 public void setLocalEchoEnabled(boolean localEchoEnabled) { 127 this.localEchoEnabled = localEchoEnabled; 128 } 129 130 /** 131 * Gets the {@link ConversationPrefix} that prepends all output from this 132 * conversation. 133 * 134 * @return The ConversationPrefix in use. 135 */ 136 public ConversationPrefix getPrefix() { 137 return prefix; 138 } 139 140 /** 141 * Sets the {@link ConversationPrefix} that prepends all output from this 142 * conversation. 143 * 144 * @param prefix The ConversationPrefix to use. 145 */ 146 void setPrefix(ConversationPrefix prefix) { 147 this.prefix = prefix; 148 } 149 150 /** 151 * Adds a {@link ConversationCanceller} to the cancellers collection. 152 * 153 * @param canceller The {@link ConversationCanceller} to add. 154 */ 155 void addConversationCanceller(ConversationCanceller canceller) { 156 canceller.setConversation(this); 157 this.cancellers.add(canceller); 158 } 159 160 /** 161 * Gets the list of {@link ConversationCanceller}s 162 * 163 * @return The list. 164 */ 165 public List<ConversationCanceller> getCancellers() { 166 return cancellers; 167 } 168 169 /** 170 * Returns the Conversation's {@link ConversationContext}. 171 * 172 * @return The ConversationContext. 173 */ 174 public ConversationContext getContext() { 175 return context; 176 } 177 178 /** 179 * Displays the first prompt of this conversation and begins redirecting 180 * the user's chat responses. 181 */ 182 public void begin() { 183 if (currentPrompt == null) { 184 abandoned = false; 185 currentPrompt = firstPrompt; 186 context.getForWhom().beginConversation(this); 187 } 188 } 189 190 /** 191 * Returns Returns the current state of the conversation. 192 * 193 * @return The current state of the conversation. 194 */ 195 public ConversationState getState() { 196 if (currentPrompt != null) { 197 return ConversationState.STARTED; 198 } else if (abandoned) { 199 return ConversationState.ABANDONED; 200 } else { 201 return ConversationState.UNSTARTED; 202 } 203 } 204 205 /** 206 * Passes player input into the current prompt. The next prompt (as 207 * determined by the current prompt) is then displayed to the user. 208 * 209 * @param input The user's chat text. 210 */ 211 public void acceptInput(String input) { 212 if (currentPrompt != null) { 213 214 // Echo the user's input 215 if (localEchoEnabled) { 216 context.getForWhom().sendRawMessage(prefix.getPrefix(context) + input); 217 } 218 219 // Test for conversation abandonment based on input 220 for(ConversationCanceller canceller : cancellers) { 221 if (canceller.cancelBasedOnInput(context, input)) { 222 abandon(new ConversationAbandonedEvent(this, canceller)); 223 return; 224 } 225 } 226 227 // Not abandoned, output the next prompt 228 currentPrompt = currentPrompt.acceptInput(context, input); 229 outputNextPrompt(); 230 } 231 } 232 233 /** 234 * Adds a {@link ConversationAbandonedListener}. 235 * 236 * @param listener The listener to add. 237 */ 238 public synchronized void addConversationAbandonedListener(ConversationAbandonedListener listener) { 239 abandonedListeners.add(listener); 240 } 241 242 /** 243 * Removes a {@link ConversationAbandonedListener}. 244 * 245 * @param listener The listener to remove. 246 */ 247 public synchronized void removeConversationAbandonedListener(ConversationAbandonedListener listener) { 248 abandonedListeners.remove(listener); 249 } 250 251 /** 252 * Abandons and resets the current conversation. Restores the user's 253 * normal chat behavior. 254 */ 255 public void abandon() { 256 abandon(new ConversationAbandonedEvent(this, new ManuallyAbandonedConversationCanceller())); 257 } 258 259 /** 260 * Abandons and resets the current conversation. Restores the user's 261 * normal chat behavior. 262 * 263 * @param details Details about why the conversation was abandoned 264 */ 265 public synchronized void abandon(ConversationAbandonedEvent details) { 266 if (!abandoned) { 267 abandoned = true; 268 currentPrompt = null; 269 context.getForWhom().abandonConversation(this); 270 for (ConversationAbandonedListener listener : abandonedListeners) { 271 listener.conversationAbandoned(details); 272 } 273 } 274 } 275 276 /** 277 * Displays the next user prompt and abandons the conversation if the next 278 * prompt is null. 279 */ 280 public void outputNextPrompt() { 281 if (currentPrompt == null) { 282 abandon(new ConversationAbandonedEvent(this)); 283 } else { 284 context.getForWhom().sendRawMessage(prefix.getPrefix(context) + currentPrompt.getPromptText(context)); 285 if (!currentPrompt.blocksForInput(context)) { 286 currentPrompt = currentPrompt.acceptInput(context, null); 287 outputNextPrompt(); 288 } 289 } 290 } 291 292 public enum ConversationState { 293 UNSTARTED, 294 STARTED, 295 ABANDONED 296 } 297 }